自定义数据库持久层框架

一、前言

本文自定义的数据库持久层框架是参考Mybatis思想设计,本意并非想去重复造轮子,而是希望从框架实现原理角度出发分析其设计思想,以便对Mybatis有更深层次的理解,也有助于看其他框架源码。

二、JDBC存在的问题

public static void main(String[] args) throws SQLException {
     
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
     
        Class.forName("com.mysql.jdbc.Driver");
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/kd-jdbc", "root", "123456");
        String sql = "select * from user where username = ?";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, "xian");
        resultSet = preparedStatement.executeQuery();
        ArrayList<User> users = new ArrayList<User>();
        while (resultSet.next()) {
     
            User user = new User();
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            user.setId(id);
            user.setUsername(username);
            users.add(user);
        }
    } catch (Exception e) {
     
        e.printStackTrace();
    }finally {
     
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

以上是一段常见的原生jdbc查询的代码,可以发现实现是很不优雅的,主要存在的问题有:

  • 频繁的创建数据库连接,造成系统资源浪费
  • SQL语句硬编码在代码中,与业务代码耦合
  • PreparedStatement参数绑定的过程存在硬编码,对于一些where动态条件,硬编码不利于代码维护
  • 对SQL结果集映射存在硬编码

三、问题的解决思路

对于以上的问题,数据库频繁创建、释放资源可以通过接入线程池处理;硬编码问题,可以将需要频繁改动的代码抽离成xml文件,使用dom4j去解析节点;而对于结果集映射,则可以通过对象的反射和内省去完成。同时,要求使用端提供核心配置文件SqlMapConfig.xml和Mapper.xml,而对于框架则需要读取相应的配置信息,完成数据源的构建,SQL语句的解析以及结果集的映射。

四、代码实现

4.1 使用端核心配置文件

SqlMapConfig.xml

<configuration>
    
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver">property>
        <property name="username" value="root">property>
        <property name="jdbcUrl" value="jdbc:mysql:///kd-jdbc">property>
        <property name="password" value="123456">property>
    dataSource>
    
    <mapper resource="UserMapper.xml">mapper>
configuration>

UserMapper.xml

<mapper namespace="com.keduw.dao.UserDao">
    
    <select id="findAll" resultType="com.keduw.pojo.User" >
        select * from user
    select>
    
    <select id="findById" resultType="com.keduw.pojo.User" paramterType="com.keduw.pojo.User">
        select * from user where id = #{id}
    select>
mapper>
4.2 封装数据源信息和查询语句

定义一个Configuration用于保存解析出来的数据源信息和执行语句,执行语句保存在mappedStatementMap中,每一条执行语句就代表一个MappedStatement,而id则是由namespace.id组成。

public class Configuration {
     

    private DataSource dataSource;
    private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
    
    /** 省略get,set方法 **/
}

封装一个工厂类SqlSessionFactoryBuilder用于数据解析和生成SqlSession,主要使用dom4j解析配置文件,将解析出来的内容封装到Configuration中,同时创建SqlSessionFactory对象,生产SqlSession,拿到SqlSession后就可以为所欲为了。

public class SqlSessionFactoryBuilder {
     

    public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
     
        // 使用dom4j解析配置文件
        XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(in);
        // 创建sqlSessionFactory对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;
    }
}
4.3 引入数据库连接池

在解析SqlMapConfig.xml过程中,引入数据库连接池创建数据源信息,将数据源交由数据库连接池管理,解决数据库连接频繁创建连接和释放资源的问题。

public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
     
    Document document = new SAXReader().read(inputStream);
    Element rootElement = document.getRootElement();
    List<Element> list = rootElement.selectNodes("//property");
    Properties properties = new Properties();
    for (Element element : list) {
     
        String name = element.attributeValue("name");
        String value = element.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);

    // 解析标签
    List<Element> mapperList = rootElement.selectNodes("//mapper");
    for (Element element : mapperList) {
     
        String resource = element.attributeValue("resource");
        InputStream resourceAsStream = Resource.getResourceAsStream("/" + resource);
        XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
        xmlMapperBuilder.parse(resourceAsStream);
    }
    return configuration;
}
4.4 SQL解析

构建一个XmlMapperBuilder实现对mapper中各种数据节点的解析,将解析出来的数据封装在MappedStatement中,MappedStatement主要包含id标识、返回值类型、参数值类型以及SQL语句。

public class MappedStatement {
     
    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String paramterType;
    //sql语句
    private String sql;
    
    /** 省略一些set,get方法 **/
}

这里做了一些简化,实际上Mybatis会将SQL语句解析成一个个的Node节点,因为要支持一些动态查询,不过我们这里出于简单考虑,就忽略动态查询,直接用String去保存。

public void parse(InputStream inputStream) throws DocumentException {
     
    Document document = new SAXReader().read(inputStream);
    Element rootElement = document.getRootElement();
    String namespace = rootElement.attributeValue("namespace");
    
    // 解析所有的