第6讲.DAO设计

2020-08-24  本文已影响0人  祥祺

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.png

Student:

@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/账号/密码,不利于维护.
解决方案: 声明为成员变量即可.(在被类中任何地方都可以访问)
如图:

refactor_1.png

问题2:问题1的解决方案有问题.
每个DAO实现类里都有一模一样的4行代码.(如右图),不利于维护(考虑有100个DAO实现类,就得重复99次).
解决方案: 把驱动名称/url/账号/密码这四行代码,专门抽取到一个JDBC的工具类中.---->JdbcUtil.
如图:

refactor_2.png

问题3:其实DAO方法,每次操作都只想需要Connection对象即可,而不关心是如何创建的.
解决方案:把创建Connection的代码,抽取到JdbcUtil中,并提供方法getConn用于向调用者返回Connection对象即可.
如图:
问题4:每次调用者调用getConn方法的时候,都会创建一个Connection对象.
但是,每次都会加载注册驱动一次.--->没必要的.
解决方案:把加载注册驱动的代码放在静态代码块中--->只会在所在类被加载进JVM的时候,执行一次.
如图:

refactor_3.png

问题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的区别:

拼接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 方法来提供。

上一篇下一篇

猜你喜欢

热点阅读