美文网首页
第6讲.DAO设计

第6讲.DAO设计

作者: 祥祺 | 来源:发表于2020-08-24 10:13 被阅读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的区别:

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

    相关文章

      网友评论

          本文标题:第6讲.DAO设计

          本文链接:https://www.haomeiwen.com/subject/gzpayqtx.html