Java精进-手写持久层框架

Posted 程序员yqy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java精进-手写持久层框架相关的知识,希望对你有一定的参考价值。

文章目录


前言

本文适合有一定java基础的同学,通过自定义持久层框架,可以更加清楚常用的mybatis等开源框架的原理。


JDBC操作回顾及问题分析

学习java的同学一定避免不了接触过jdbc,让我们来回顾下初学时期接触的jdbc操作吧

以下代码连接数据库查询用户表信息,用户表字段分别为用户id,用户名username。

Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        User user = new User();
        try 
            // 加载数据库驱动
            //Class.forName("com.mysql.jdbc.Driver");
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 通过驱动管理类获取数据库链接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "mimashi3124");
            // 定义sql语句?表示占位符
            String sql = "select * from user where username = ?";
            // 获取预处理statement
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
            preparedStatement.setString(1, "盖伦");
            // 向数据库发出sql执⾏查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            // 遍历查询结果集
            while (resultSet.next()) 
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                // 封装User
                user.setId(id);
                user.setUsername(username);
            
            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操作数据库存在以下问题:

  1. 数据库连接创建、释放频繁造成系统资源浪费,从⽽影响系统性能。
  2. Sql语句我们是写在代码里的,代码不容易维护,实际应⽤中sql变化的可能较⼤,sql变动需要改变 java代码。
  3. 使⽤preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可能多也可能少,修改sql还要修改代码,系统不易维护。
  4. 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据 库记录封装成pojo对象解析⽐较⽅便

问题解决思路

  1. 使⽤数据库连接池初始化连接资源,避免资源浪费
  2. 将sql语句抽取到xml配置中,这种sql的变动只用关注xml文件,不比去一堆java代码里改写sql
  3. 参数硬编码问题可以使用反射、内省等技术、自动将实体与表字段进行映射。

自己动手写个持久层框架

接下来,我们来一个个解决上面的问题

数据库连接池我们可以直接使用c3p0提供的ComboPooledDataSource即可

为了解决sql硬编码问题,我们要把sql写到xml文件中,那自然是要定义一个xml文件了。

光有sql肯定不行,毕竟我们要先连接数据库,sql语句才有存在的意义。所以xml中得先定义数据配置信息,然后才是sql语句。

1.定义配置xml文件

我们新建一个sqlMapConfig.xml,定义数据源信息、并且增加两个sql语句,parameterType为sql执行参数,resultType为方法返回实体。

代码如下(数据库不同版本使用驱动类可能不同):

<configuration>
    <!--数据库连接信息-->
    <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<!--    <property name="driverClass" value="com.mysql.jdbc.Driver"/>-->
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai"/>
    <property name="username" value="root"/>
    <property name="password" value="mimashi3124"/>

	<select id="selectOne" parameterType="org.example.pojo.User"
            resultType="org.example.pojo.User">
        select * from user where id = #id and username =#username
    </select>

    <select id="selectList" resultType="org.example.pojo.User">
        select * from user
    </select>
</configuration>

现在xml文件数据库信息也有了,sql语句定义也有了,还有什么问题呢?

我们实际中对sql的操作会涉及到不同的表,所以我们改进一下,把每个表的sql语句单独放在一个xml里,这样结构更清晰就容易维护。

优化以后的xml配置现在是这样了

sqlMapConfig.xml

<configuration>
    <!--数据库连接信息-->
    <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
	<!-- <property name="driverClass" value="com.mysql.jdbc.Driver"/>-->
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai"/>
    <property name="username" value="root"/>
    <property name="password" value="mimashi3124"/>
    <!--引⼊sql配置信息-->
	<mapper resource="mapper.xml"></mapper>
</configuration>

mapper.xml

<mapper namespace="user">
    <select id="selectOne" parameterType="org.example.pojo.User"
            resultType="org.example.pojo.User">
        select * from user where id = #id and username =#username
    </select>

    <select id="selectList" resultType="org.example.pojo.User">
        select * from user
    </select>
</mapper>

顺便定义一下业务实体User

public class User 
    private int id;
    private String username;
    
    public int getId() 
        return id;
    

    public void setId(int id) 
        this.id = id;
    

    public String getUsername() 
        return username;
    

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

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

2.读取配置文件

读取完成以后以流的形式存在,不好操作,所以我们要做解析拿到信息,创建实体对象来存储。

  1. Configuration : 存放数据库基本信息、Map<唯⼀标识,Mapper> 唯⼀标识:namespace + “.” + id
  2. MappedStatement:存放sql语句、输⼊参数类型、输出参数类型

xml解析我们使用dom4j

首先引入maven依赖

代码如下(mysql驱动版本根据实际使用mysql版本调整):

<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
    </dependencies>

数据库配置实体 Configuration

public class Configuration 
    //数据源
    private DataSource dataSource;
    //map集合: key:statementId value:MappedStatement
    private Map<String,MappedStatement> mappedStatementMap = new HashMap<>();

    public DataSource getDataSource() 
        return dataSource;
    

    public Configuration setDataSource(DataSource dataSource) 
        this.dataSource = dataSource;
        return this;
    

    public Map<String, MappedStatement> getMappedStatementMap() 
        return mappedStatementMap;
    

    public Configuration setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) 
        this.mappedStatementMap = mappedStatementMap;
        return this;
    

Sql语句信息实体

public class MappedStatement 

    //id
    private String id;
    //sql语句
    private String sql;
    //输⼊参数
    private String parameterType;
    //输出参数
    private String resultType;

    public String getId() 
        return id;
    

    public MappedStatement setId(String id) 
        this.id = id;
        return this;
    

    public String getSql() 
        return sql;
    

    public MappedStatement setSql(String sql) 
        this.sql = sql;
        return this;
    

    public String getParameterType() 
        return parameterType;
    

    public MappedStatement setParameterType(String parameterType) 
        this.parameterType = parameterType;
        return this;
    

    public String getResultType() 
        return resultType;
    

    public MappedStatement setResultType(String resultType) 
        this.resultType = resultType;
        return this;
    

顺便定义一个Resources类来读取xml文件流

public class Resources 
    public static InputStream getResourceAsSteam(String path) 
        return Resources.class.getClassLoader().getResourceAsStream(path);
    

接下来就是实际的解析了,因为解析代码比较多,我们考虑封装类单独处理解析

定义XMLConfigBuilder类解析数据库配置信息

public class XMLConfigBuilder 

    private Configuration configuration;


    public XMLConfigBuilder() 
        this.configuration = new Configuration();
    

    public Configuration parserConfiguration(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException 
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        List<Element> propertyElements = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element propertyElement : propertyElements) 
            String name = propertyElement.attributeValue("name");
            String value = propertyElement.attributeValue("value");
            properties.setProperty(name,value);
        
        //解析到数据库配置信息,设置数据源信息
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
		
        configuration.setDataSource(comboPooledDataSource);

		//将configuration传入XMLMapperBuilder中做sql语句解析。
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(this.configuration);
        List<Element> mapperElements = rootElement.selectNodes("//mapper");
        for (Element mapperElement : mapperElements) 
            String mapperPath = mapperElement.attributeValue("resource");
            InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(mapperPath);
            xmlMapperBuilder.parse(resourceAsStream);
        
        return configuration;
    

定义XMLMapperBuilder类解析数据库配置信息

public class XMLMapperBuilder 

    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration) 
        this.configuration = configuration;
    
    public void parse(InputStream inputStream) throws DocumentException,
            ClassNotFoundException 
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List<Element> select = rootElement.selectNodes("select");
        for (Element element : select)  //id的值
            String id = element.attributeValue("id");
            String parameterType = element.attributeValue("parameterType"); //输⼊参数
            String resultType = element.attributeValue("resultType"); //返回参数
            //statementId,后续调用通过statementId,找到对应的sql执行
            String key &

以上是关于Java精进-手写持久层框架的主要内容,如果未能解决你的问题,请参考以下文章

Java精进-手写持久层框架

简单实现自定义持久层框架,手写MyBatis实现基础功能

手写Mybatis,彻底搞懂框架原理

从零开始手写 mybatisMVP 版本

从零开始手写 mybatisMVP 版本

了解mybatis源码手写mybatis