一、持久层框架设计实现及MyBatis源码分析-自定义持久层框架(一)

首先,带上先带上问题进行思考,既然JDBC已经能完成与数据库的交互,已经能够完成对数据库的CRUD操作,为什么后期还会出现Mybatis呢?
是不是意味着JDBC在与数据库进行交互时,本身还是存在一些问题,正是因为存在问题,所以后期出现了很多持久层框架,将JDBC对数据库的操作进行了封装,在封装过程中,对JDBC存在的问题进行规避和解决,而Mybatis只是众多持久层框架其中之一
接下来新建一个简单的maven项目,对JDBC代码进行一个简单的回顾,并分析JDBC在与数据库进行交互时究竟存在哪些问题,然后给出对应问题的解决思路,并在此基础上完成一个自定义持久层框架(为什么需要完成这样一个自定义这样一个持久层框架呢,其实此框架就是Mybatis的一个雏形,对后期翻阅Mybatis源码会大有帮助)

一、JDBC代码回顾及问题分析

新建一个maven项目,项目中的pom.xml文件内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>study.lagou.com</groupId>
    <artifactId>jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

    </dependencies>
</project>

User实体对象

package study.lagou.com.jdbc.pojo;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-26 0:18
 */
public class User {
    /**
     * 主键信息
     */
    private Integer id;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 用户密码
     */
    private String password;
    /**
     * 用户昵称
     */
    private String nickname;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}

JDBC连接数据操作类

package study.lagou.com.jdbc;
import study.lagou.com.jdbc.pojo.User;
import java.sql.*;

/**
 * @Description: 功能描述
 * @Author houjh
 * @Email: happyxiaohou@gmail.com
 * @Date: 2021-1-25 23:32
 */
public class JDBCTest {
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //通过驱动管理类获取到一个connection数据库连接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC",
                    "root","111111");
            //编写对应的SQL语句,其中?表示参数的占位符
            String sql = "select * from user where username = ?";
            //通过connection和sql获取到数据库预处理对象PreparedStatement
            preparedStatement = connection.prepareStatement(sql);
           //借助数据库预处理对象设置参数,第一个参数为SQL语句中参数的序号(从1开始),第二个参数为设置的参数值
            preparedStatement.setString(1,"zhangsan");
            //向数据库发出SQL执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //遍历查询的结果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封装User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }
        } catch (Exception e){
           e.printStackTrace();
        } finally {
            //释放资源
            if(resultSet != null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(preparedStatement != null){
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(connection != null){
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JDBC 代码基本回顾完成,接下来我们对代码进行问题分析

//加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//通过驱动管理类获取到一个connection数据库连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC","root","111111");

首先分析此段代码,我们将数据库的连接驱动和数据库连接信息编写在了JAVA代码当中,存在硬编码问题,如果后期连接的数据库发生改变,我们将要在JAVA源代码中修改数据库连接驱动和数据库连接配置信息,修改完成后我们必须重新编译JAVA源文件,并再次打包部署,整个过程非常麻烦

其次,这句代码如果是在实际项目中使用,我们需要将此段代码写到持久层方法当中,如果该持久层框架被请求多次,则意味着这段代码也会被执行多次,而每一次执行我们都获取到一个新的数据库连接,然后去执行SQL,最终再释放这个连接,如此反复,每一次请求都开启一个新的连接,造成了数据库连接资源的浪费(数据库连接可以算做是一个非常宝贵的资源,在我们获取数据库连接的时候,底层需要先去建立TCP连接,完成三次握手,整个过程比较耗费资源,影响性能)

针对以上代码,我们总结出两个问题
1、数据库配置信息存在硬编码问题
2、频繁创建和释放数据库连接

            //编写对应的SQL语句,其中?表示参数的点位符
            String sql = "select * from user where username = ?";
            //通过connection获取到数据库预处理对象PreparedStatement
            preparedStatement = connection.prepareStatement(sql);
            //借助数据库预处理对象设置参数,第一个参数为SQL语句中参数的序号(从1开始),第二个参数为设置的参数值
            preparedStatement.setString(1,"zhangsan");
            //向数据库发出SQL执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //遍历查询的结果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封装User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }

再分析上一段代码,所存在的问题是
3、SQL语句、设置参数、获取结果集参数均存在硬编码问题

            //遍历查询的结果集
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                String nickname = resultSet.getString("nickname");
                // 封装User
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setNickname(nickname);
                System.out.println(user);
            }

最后看封装返回结果集对象这段,现在看到的是User对象属性值比较少的情况,但是如果试想,如果User对象存在几十、上百个属性值的时候,则在此处设置对象属性值的过程将变得相当繁琐,所以此段代码我们分析出的问题就是
4、需要手动封装返回结果集,较为繁琐

通过对JDBC连接代码块进行分析,JDBC连接操作数据库时主要存在以下问题:
1、 频繁创建、释放数据库连接,造成系统资源浪费,从⽽影响系统性能。
2、 sql语句在代码中硬编码,造成代码不易维护,实际应⽤中sql变化的可能较大,sql变动需要改变java代码。
3、 使⽤preparedStatement向占位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可能多也可能少,修改sql还要修改代码,系统不易维护。
4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便

二、针对分析出的问题,给出问题解决思路

1、数据库配置信息存在硬编码问题
看到硬编码,我们容易联想到配置文件,此处给出的解决方案也是通过配置文件解决硬编码问题

2、频繁创建和释放数据库连接
此处问题我们可以通过数据库连接池来进行解决

3、SQL语句、设置参数、获取结果集参数均存在硬编码问题
同样通过配置文件来处理,但是此处配置文件建议和问题1中的配置文件分开来处理,因为问题1中的配置文件,保存的是数据库连接的基本信息,相对来说内容比较固定,不容易发生改变,而问题3中所使用的配置文件,主要是用来存储SQL语句、参数、返回结果集等信息,比较容易发生改变,所以建议和问题1中的配置文件分开

4、需要手动封装返回结果集,较为繁琐
针对这个问题,我们可以通过使用反射、内省等技术去完成查询出来的结果集与实体中属性的自动转换封装

三、自定义框架设计

前面进行了JDBC代码回顾以及问题分析,并给出了解决问题的基本思路,接下来我们设置一个自定义的持久层框架(注意:当前我们自定义的持久层框架,它的本质是对JDBC代码的一个封装,只不过在封装的过程当中,我们需要对JDBC代码存在的问题进行规避或解决)

1、自定义持久层框架的设计思路

自定义持久层框架思路.png

自定义的框架分为使用端自定义持久层框架本身两部分

1.1、使用端

使用端这个项目当中我们将会使用自定义持久层框架来完成对数据库的交互(对数据库表的CRUD操作),在使用端需要对自定义持久层框架进行调用,那么使用端项目需要引入自定义持久层框架的jar包(自定义持久层框架也是一个项目,使用端需要调用自定义持久层框架中的方法,则需要引入对应的jar包)

一段JDBC代码,想要正常执行,那么必不可少的是两部分信息,一个是数据连接配置信息,一个是SQL配置信息,所以使用端需要提供这两部分配置信息,其中SQL配置信息包括SQL语句、参数类型以及返回值类型,因为我们要对JDBC硬编码的问题进行解决,所以我们通过配置文件来提供这两部分信息
(1)、sqlMapConfig.xml:存放数据库配置信息,存放mapper.xml的全路径
(2)、mapper.xml:存放SQL配置信息

1.2、持久层框架本身

持久层框架本身也是一个项目,项目主要实现的功能就是对JDBC代码进行封装,这就意味着当使用端对自定义持久层框架进行调用时,底层执行的还是JDBC代码,因为配置文件存入在使用端的配置文件当中,所以自定义持久层框架需要做的事情就是通过路径读取到对应的配置文件,读取到对应配置文件的内容,那么就可以调用底层的JDBC代码进行对数据库的操作了(这里就是整个自定义持久层框架的一个基本思路)

有了自定义持久层框架的基本思路,那么接下来我们对具体的操作进行细化
(1)、加载配置文件(根据配置文件的路径,将sqlMapConfig.xml和mapper.xml配置文件加载成字节输入流,以流的形式存放在内存当中)
具体的做法就是创建一个Resources类,在这个类中有一个方法,InputStream in = getResourceAsStream(String path),并且这个方法参见要传递一个参数path,这个path就是sqlMapConfig.xml和mapper.xml配置文件所存放的路径(这里引发一个问题思考,我们在使用端有两个配置文件,那么自定义持久层框架也要对配置文件加载2次呢?答案是可以加载2次,但是不建议加载2次,我们可以在sqlMapConfig.xml文件中引入mapper.xml文件的全路径,在读取sqlMapConfig.xml这个配置文件的时候,就可以将mapper.xml文件的全路径也读取出来,拿到文件路径了,就方便对文件进行操作了)

(2)、创建两个javaBean(由于我们从配置文件加载出来的配置文件,是以流的形式存放到内存当中的,不方便进行操作,所以我们需要创建两个容器对象,将解析出来的配置文件的内容存放到这两个容器对象中,方便操作)
Configuration:核心配置类:存放sqlMapConfig.xml解析出来的内容
MappedStatement:映射配置类:存放mapper.xml解析出来的内容

(3)、解析配置文件(使用dom4j来对配置文件进行解析)
创建一个SqlSessionFactoryBuilder类,该类中有一个build(Inputstream in)方法,在build方法中主要完成两件事
第一:使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
第二:创建SqlSessionFactory工厂对象,主要用来生产sqlSession会话对象,此处理用到工厂设计模式(工厂设计模式可以降低代码间耦合度,并根据需求生产出不同状态类型的对象)

(4)、创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory
接口中定义openSession()方法,该方法的主要作用是生产sqlSession

(5)、创建SqlSession接口及实现类DefaultSqlSession
定义对数据库的CRUD操作,定义selectList()、selectOne()、update()、delete()等方法,在这些方法的实现当中可以直接通过操作JDBC代码来实现对数据库的操作,但是由于存在大量的初始化JDBC配置的重复操作,所以衍生出后续一步的优化

(6)、创建Executor接口及实现类SimpleExecutor实现类
定义一个query(Configuration configuration,MappedStatement mappedStatement,Object... params)方法,该方法执行的就是JDBC代码,由于执行JDBC代码需要对应的配置文件,所以将两个配置bean传入进来,最后一个参数表示查询时需要拼接的参数,由于不知道具体的参数个数,所以直采用可变参数的写法来处理

下一篇笔记地址:https://www.jianshu.com/p/d9c7b85d70b2

具体代码对应下载地址:https://gitee.com/happymima/mybatis.git