第6讲.DAO设计
DAO设计
DAO
单纯的使用JDBC规范操作数据库存在的问题
我们通过一张图来看一下单纯的使用JDBC规范来操作数据库,在代码上存在哪些不足。如图
1.png
代码重复问题解决思路
JDBC操作数据库出现了代码重复问题,怎么解决?
思路:
从以前学习的集合上寻找线索,
因为集合和数据库 都是容器,用来存储数据的,
分析:没有学习List集合之前,那么是使用什么容器来存储对象的,使用 的是数组存放的对象的。
有了List集合以后,存储对象的方式发生了什么样的变化
2.png
我们就可以模仿着对JDBC对代码进行抽取,如图:
3.png
为何要学习DAO?
DAO(DataAccess Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露数据库实现细节的前提下提供了各种数据操作。为了建立一个健壮的 Java EE 应用,应该将所有对数据源的访问操作进行抽象化后封装在一个公共 API 中。
用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO 模式实际上包含了两个模式,一是 Data Accessor(数据访问器),二是 Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。
我们可以先通过一张图在没有DAO的使用的情况下来说明代码存在的问题
4.png什么是DAO?
DAO(Data Access Object)是一个数据访问接口,数据访问:顾名思义就是与数据库打交道。夹在业务逻辑与数据库资源中间。
DAO规范和设计
DAO的规范
DAO其实是一个组件(可以重复使用),包括:接口和接口的实现类
分包规范:
域名倒写.项目模块名.组件;
cn.wolfcode.smis.domain; // 该包存放的是domain类(javaBean)
cn.wolfcode.smis.dao; // 该包存放的都是接口,接口中都是操作数据库抽象方法
cn.wolfcode.smis.dao.impl;// 该包存放的都是dao包中的接口的实现类
cn.wolfcode.smis.test; // 该包存放的是测试类 用来测试接口中的方法
类名和接口规范:
以t_student 为例:
domain:
存放的都是javaBean(严格要求必须符合javaBean的规范)必须提供字段的setter/getter 方法 例如: Student,Employee
dao :
存放的都是接口,对一个表的增删改查操作 。注意 : 在写法上,一般都以I开头
以DAO为结尾 例如; IStudentDAO ,IEmployeeDAO。 IXxxDAO/
dao.impl:
存放的都是dao包中接口的实现类。注意:在写法上, 一般以 Impl为结尾。
例如:StudentDAOImpl ,EmployeeDAOImpl
test:
存放dao包中接口对应的测试类,用来测试接口中的方法。注意:在写法上,测试类以Test为结尾,方法以test开头
例如 StudentTest
IStudentDAO studentDAO = new StudentDAOImpl();
studentDAO.save(...);
良好的编码顺序:
1. 先根据表来创建domain包以及对象。
2. 根据domain对象来创建dao包以及接口。
接口的方法(增删改查), 接口的方法需要规范。
3. 生成实现类(先空实现)。
4. 生成测试类。
5. 测试类是 测试dao的,所以可以在测试类中先去创建一个dao对象。
6. 实现类的某一个方法,实现一个,测试一个。
DAO组件中的方法设计
既然是方法设计,那么我们首先要搞清楚 方法的组成。
方法的组成:
修饰方法符 返回值类型 方法名称 (参数类型 参数值)
以t_student 表为例
保存方法的设计
保存数据到数据库的sql:
insert into t_student values(null,'王伟',18);
保存方法设计:
public void save(String name,Integer age);
如果参数过多,会导致代码可读性差,想到了java中的封装思想
@AllArgsConstructor
public class Student{
private Long id;
private String name;
private Integer age;
}
创建Student对象
Student s = new Student(null,”王伟”,18);
保存方法的最终设计
public void save(Student s);
sava.png
修改方法的设计
修改数据的sql:
update t_student set name=”马云”,age=20 where id=1;
修改方法的设计:
Public void update(Long id , String name, int age);
思想和我们设计保存方法是一样的。 如果参数过多,会导致代码可读性差,想到了java中的封装思想
修改方法的最终设计
public void update( Student s); s表示修改的内容 里面封装了修改的条件id
update.png
删除方法的设计
删除数据的sql:
delete from t_student where id=1
删除方法的设计:
public void delete (Long id); id:表示删除的条件
delete.png
查询单条数据方法的设计
获取单条记录的sql:
select * from t_student where id=1
考虑到获取的数据很多,考虑到java中的封装思想,对结果进行封装
获取单条记录的方法:
public Student get(Long id);
get.png
查询多条数据方法的设计
获取所有记录的sql:
select * from t_student;
考虑到获取的数据很多,考虑到java中的封装思想,对结果进行封装
获取所有数据的方法:
public List<Student> listAll();
listAll.png
使用DAO的规范来完成CRUD
在处理查询功能的时候,根据Java的封装思想,我们应该把结果进行封装到对象中。如图:
resultSet.pngStudent:
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private Long id;
private String name;
private Integer age;
}
IStudentDAO:
public interface IStudentDAO {
/**
* 保存指定的学生对象
* @param s 需要保存的学生对象
*/
void save(Student s);
/**
* 修改指定id的学生对象
* @param s 新的Student对象(包含删除的条件id)
*/
void update(Student s);
/**
* 删除指定id的学生对象
* @param id 需要被删除的学生的id
*/
void delete(Long id);
/**
* 查询指定id的学生对象
* @param id 需要查询的学生对象的id
* @return 如果该id对应的学生存在,则返回,否则返回null
*/
Student get(Long id);
/**
* 查询所有的学生对象
* @return 返回所有学生的对象集合,如果没有学生对象,返回一个空集合
*/
List<Student> listAll();
}
StudentDAOImpl:
public class StudentDAOImpl implements IStudentDAO {
@Override
public void save(Student s) {
String sql = "insert into t_student(name,age) values(?,?)";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, s.getName());
ps.setInt(2, s.getAge());
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.closeResources(ps, connection);
}
}
@Override
public void update(Student s) {
String sql = "update t_student set name=? , age=? where id=?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, s.getName());
ps.setInt(2, s.getAge());
ps.setLong(3, s.getId());
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.closeResources(ps, connection);
}
}
@Override
public void delete(Long id) {
String sql = "delete from t_student where id =?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ps.setLong(1, id);
ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.closeResources(ps, connection);
}
}
@Override
public Student get(Long id) {
Student student = new Student();
String sql = "select * from t_student where id =?";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ps.setLong(1, id);
ResultSet resultSet = ps.executeQuery();
if (resultSet.next()) {
student.setName(resultSet.getString("name"));
student.setAge(resultSet.getInt("age"));
student.setId(resultSet.getLong("id"));
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.closeResources(ps, connection);
}
return student;
}
@Override
public List<Student> listAll() {
List<Student> list = new ArrayList<Student>();
String sql = "select * from t_student";
Connection connection = JdbcUtil.getConnection();
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery();
while (resultSet.next()) {
Student student = new Student();
student.setName(resultSet.getString("name"));
student.setAge(resultSet.getInt("age"));
student.setId(resultSet.getLong("id"));
list.add(student);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.closeResources(ps, connection);
}
return list;
}
}
StudentDAOTest:
public class StudentDAOTest {
private IStudentDAO studentDAO = new StudentDAOImpl();
@Test
public void testSave() {
Student s = new Student();
s.setAge(19);
s.setName("郭美美");
studentDAO.save(s);
}
@Test
public void testUpdate() {
Student s = new Student();
s.setAge(19);
s.setId(4L);
s.setName("郭美美");
studentDAO.update(s);
}
@Test
public void testDelete() {
studentDAO.delete(4L);
}
@Test
public void testGet() {
Student student = studentDAO.get(5L);
System.out.println(student);
}
@Test
public void testListAll() {
List<Student> list = studentDAO.listAll();
list.stream().forEach(System.out::println);
}
}
重构设计
什么是重构?
重构(Refactoring)就是通过调整程序代码,改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
抽取JdbcUtil工具类
上述的DAO方法中的代码,存在的问题:
问题1:每个DAO方法中都会写:驱动名称/url/账号/密码,不利于维护.
解决方案: 声明为成员变量即可.(在被类中任何地方都可以访问)
如图:
问题2:问题1的解决方案有问题.
每个DAO实现类里都有一模一样的4行代码.(如右图),不利于维护(考虑有100个DAO实现类,就得重复99次).
解决方案: 把驱动名称/url/账号/密码这四行代码,专门抽取到一个JDBC的工具类中.---->JdbcUtil.
如图:
问题3:其实DAO方法,每次操作都只想需要Connection对象即可,而不关心是如何创建的.
解决方案:把创建Connection的代码,抽取到JdbcUtil中,并提供方法getConn用于向调用者返回Connection对象即可.
如图:
问题4:每次调用者调用getConn方法的时候,都会创建一个Connection对象.
但是,每次都会加载注册驱动一次.--->没必要的.
解决方案:把加载注册驱动的代码放在静态代码块中--->只会在所在类被加载进JVM的时候,执行一次.
如图:
问题5:每个DAO方法都要关闭资源.(鸡肋代码).
解决方案:把关闭资源的代码,抽取到JdbcUtil中.
public static void close(Connection conn, Statement st, ResultSet rs) {}
调用者:
DML: JdbcUtil.close(conn,st,null);
DQL: JdbcUtil.close(conn,st,rs);
如图:
refactor_4.png抽取db.properties文件
问题6:在JdbcUtil中存在这硬编码(连接数据库的四要素),还是不利于维护.
解决方案:把数据库的信息专门的提取到配置文件中去.那么以后就只需要该配置文件即可.
db.properties文件存放于source folder目录(resources):
如图:
refactor_5.png重构完成的代码如下:
JdbcUtil:
public final class JdbcUtil {
private JdbcUtil() {
}
private static Properties properties = new Properties();
/*当该工具类加载进内存时,
会立即执行static代码块中的代码
并且只执行一次*/
static {
try {
// 加载配置文件
InputStream inputStream = Thread.currentThread().getContextClassLoader()// 获取当前线程的类加载器
.getResourceAsStream("db.properties"); // 加载classPath路径下面的配置文件
properties.load(inputStream);
// 加载驱动
Class.forName(properties.getProperty("dirverClassName"));
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取Connection连接对象
public static Connection getConnection() {
try {
Connection conn = DriverManager.getConnection(// 连接数据库的要素
properties.getProperty("url"), // url
properties.getProperty("username"), // 用户名
properties.getProperty("password")); // 密码
return conn;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 关闭资源
public static void closeResources(ResultSet resultSet, Statement st, Connection connection) {
try {
if (resultSet != null) {
resultSet.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (st != null) {
st.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
// 关闭资源
public static void closeResources(Statement st, Connection connection) {
closeResources(null, st, connection);
}
}
预编译语句对象
为何要使用预编译语句对象(PreparedStatement)
Statement和PreparedStatement的区别:
-
1). PreparedStatement 代码的可读性和可维护性. (SQL模板,使用占位符表示参数)
-
2).PreparedStatement 能最大可能提高性能. MySQL不支持.
-
3).PreparedStatement 能保证安全性.
可以防止SQL注入: 选择:使用PreparedStatement.
拼接SQL上,操作更简单(可读和维护性)
性能问题
预编译的性能会更加高效(但是需要取决于数据库服务器是否支持预编译)
从测试的结果来看:MySQL不支持预编译(其实MySQL5.x开始是支持的,但是默认是关闭的,因为开启后效果也不明显. 所以一般我们就认为它不支持,Oracle中效果非常明显)
如图:
performance.png
可以防止QL注入
需求:
完成一个登陆案例
需求分析:
其实是一个查询的操作,根据账号-和密码 作为条件 去数据库中查找有没有该用户信息,如果有,表示登陆成功,如果没有表示登陆失败
把账号的内容修改为 ’OR 1=1 OR‘ ,然后登陆也是可以登陆成功的
发现通过使用statement语句对象,会引起sql注入的问题,寻找解决方案,使用preparedStatement对象
如图:
login.png代码如下:
prepared_login.png什么是预编译语句对象(PreparedStatement)
java.sql包中的PreparedStatement 接口继承了Statement,并与之在两方面有所不同
PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句“准备好”。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN参数的值在 SQL 语句创建时未被指定。相反的,该语句为每个 IN 参数保留一个问号(“?”)作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX 方法来提供。