手写Mybatis:第5章-数据源的解析、创建和使用

文章目录

  • 一、目标:数据源的解析、创建和使用
  • 二、设计:数据源的解析、创建和使用
  • 三、实现:数据源的解析、创建和使用
    • 3.1 引入依赖
    • 3.2 工程结构
    • 3.3 数据源解析、创建和使用关系图
    • 3.4 事务管理接口和事务工厂
      • 3.4.1 事务的隔离级别
      • 3.4.2 定义事务接口
      • 3.4.3 事务接口实现类
      • 3.4.4 事务工厂
      • 3.4.5 JDBC事务工厂
    • 3.5 数据源工厂创建
      • 3.5.1 数据源工厂
      • 3.5.2 阿里的druid数据库连接池
    • 3.6 创建配置环境基础类
      • 3.6.1 绑定的SQL对象
      • 3.6.2 参数映射对象
      • 3.6.3 环境配置类
      • 3.6.4 映射器语句类
    • 3.7 类型别名注册器
      • 3.7.1 JDBC枚举类型
      • 3.7.2 类型别名注册机
      • 3.7.3 配置项
    • 3.8 解析数据源配置
      • 3.8.1 构建器基类
      • 3.8.2 XML配置构建器
    • 3.9 SQL执行和结果封装
      • 3.9.1 默认sqlSession实现类
  • 四、测试:数据源的解析、创建和使用
    • 4.1 创建 mybatis 数据库并添加数据库表
    • 4.2 提供 DAO 接口 和 User 实体类
      • 4.2.1 用户持久层
      • 4.2.2 用户类
    • 4.3 配置数据源和配置Mapper
      • 4.3.1 配置文件
      • 4.3.2 用户接口配置文件
    • 4.4 单元测试
    • 4.5 功能验证
  • 五、总结:数据源的解析、创建和使用

一、目标:数据源的解析、创建和使用

解析 XML 中关于 dataSource 数据源信息配置,并建立事务管理和连接池的启动和使用,并将这部分能力在 DefaultSqlSession 执行 SQL 语句时进行调用。

二、设计:数据源的解析、创建和使用

怎么完成对数据源的解析?

  • 建立数据源连接池和 JDBC 事务工厂操作,并以 XML 配置数据源信息为入口,在 XMLConfigBuilder 中添加数据源解析和构建操作。
  • 向配置类 configuration 添加 JDBC 操作环境信息,以便在 DefaultSqlSession 完成对 JDBC 执行 SQL 操作。

手写Mybatis:第5章-数据源的解析、创建和使用_第1张图片

  • parse 中解析 XML DB 链接配置信息,并完成事务工厂和连接池的注册环境到配置类的操作。
  • 调用 selectOne 方法的处理,把 SQL 语句放到 DB 连接池中进行执行,以及完成简单的结果封装。

三、实现:数据源的解析、创建和使用

3.1 引入依赖

pom.xml

<dependencies>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>5.1.48version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.2.9version>
    dependency>
dependencies>

3.2 工程结构

mybatis-step-04
|-src
    |-main
    |   |-java
    |       |-com.lino.mybatis
    |           |-binding
    |           |   |-MapperMethod.java
    |           |   |-MapperProxy.java
    |           |   |-MapperProxyFactory.java
    |           |   |-MapperRegistry.java
    |           |-builder
    |           |   |-xml
    |           |   |   |-XMLConfigBuilder.java
    |           |   |-BaseBuilder.java
    |           |-datasource
    |           |   |-druid
    |           |   |   |-DruidDataSourceFacroty.java
    |           |   |-DataSourceFactory.java
    |           |-io
    |           |   |-Resources.java
    |           |-mapping
    |           |   |-BoundSql.java
    |           |   |-Environment.java
    |           |   |-MappedStatement.java
    |           |   |-ParameterMapping.java
    |           |   |-SqlCommandType.java
    |           |-session
    |           |   |-defaults
    |           |   |   |-DefaultSqlSession.java
    |           |   |   |-DefaultSqlSessionFactory.java
    |           |   |-Configuration.java
    |           |   |-SqlSession.java
    |           |   |-SqlSessionFactory.java
    |           |   |-SqlSessionFactoryBuilder.java
    |           |   |-TransactionIsolationLevel.java
    |           |-transaction
    |           |   |-jdbc
    |           |   |   |-JdbcTransaction.java
    |           |   |   |-JdbcTransactionFactory.java
    |           |   |-Transaction.java
    |           |   |-TransactionFactory.java
    |           |-type
    |           |   |-JdbcType.java
    |           |   |-TypeAliasRegistry.java
    |-test
        |-java
        |   |-com.lino.mybatis.test
        |   |-dao
        |   |   |-IUserDao.java
        |   |-po
        |   |   |-User.java
        |   |-ApiTest.java
        |-resources
            |-mapper
            |   |-User_Mapper.xml
            |-mybatis-config-datasource.xml

3.3 数据源解析、创建和使用关系图

手写Mybatis:第5章-数据源的解析、创建和使用_第2张图片

  • 以事务接口 Transaction 和事务工厂 TransactionFactory 的实现,包装数据源 DruidDataSourceFactory 的功能。数据源采用阿里的 druid
  • 当所有的数据源相关功能准备好之后,在 XMLConfigBuilder 解析 XML 配置操作中,对数据源的配置进行解析以及创建出相应的服务,存在到 Configuration 的环境配置中。
  • 最后在 DefaultSqlSession#selectOne 方法中完成 SQL 的执行和结果封装,最终就把整个 Mybatis 的核心脉络串联起来。

3.4 事务管理接口和事务工厂

一次数据库的操作应该具备事务管理能力,而不是通过 JDBC 获取链接后直接执行,还应该把控链接、提交、回滚和关闭的操作处理。结合 JDBC 的能力封装事务管理。

3.4.1 事务的隔离级别

TransactionIsolationLevel.java

package com.lino.mybatis.session;

import java.sql.Connection;

/**
 * @description: 事务的隔离级别
 */
public enum TransactionIsolationLevel {

    //包括JDBC支持的5个级别
    NONE(Connection.TRANSACTION_NONE),
    READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
    READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
    REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
    SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);

    private final int level;

    TransactionIsolationLevel(int level) {
        this.level = level;
    }

    public int getLevel() {
        return level;
    }
}

3.4.2 定义事务接口

Transaction.java

package com.lino.mybatis.transaction;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * @description: 事务接口
 */
public interface Transaction {

    /**
     * 获取数据库连接
     *
     * @return 数据库连接
     * @throws SQLException SQL异常
     */
    Connection getConnection() throws SQLException;

    /**
     * 提交
     *
     * @throws SQLException SQL异常
     */
    void commit() throws SQLException;

    /**
     * 回滚
     *
     * @throws SQLException SQL异常
     */
    void rollback() throws SQLException;

    /**
     * 关闭
     *
     * @throws SQLException SQL异常
     */
    void close() throws SQLException;
}
  • 定义标准的事务接口,连接、提交、回滚、关闭,具体可以由不同的事务方式进行实现。
  • 包括:JDBC 和托管事务,托管事务是交给 Spring 容器管理。

3.4.3 事务接口实现类

JdbcTransaction.java

package com.lino.mybatis.transaction.jdbc;

import com.lino.mybatis.session.TransactionIsolationLevel;
import com.lino.mybatis.transaction.Transaction;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * @description: JDBC 事务,直接利用 JDBC 的commit、rollback。依赖于数据源获得的连接管理事务范围
 */
public class JdbcTransaction implements Transaction {

    protected Connection connection;
    protected DataSource dataSource;
    protected TransactionIsolationLevel level = TransactionIsolationLevel.NONE;
    protected boolean autoCommit;

    public JdbcTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        this.dataSource = dataSource;
        this.level = level;
        this.autoCommit = autoCommit;
    }

    public JdbcTransaction(Connection connection) {
        this.connection = connection;
    }

    @Override
    public Connection getConnection() throws SQLException {
        connection = dataSource.getConnection();
        connection.setTransactionIsolation(level.getLevel());
        connection.setAutoCommit(autoCommit);
        return connection;
    }

    @Override
    public void commit() throws SQLException {
        if (connection != null && !connection.getAutoCommit()) {
            connection.commit();
        }
    }

    @Override
    public void rollback() throws SQLException {
        if (connection != null && !connection.getAutoCommit()) {
            connection.rollback();
        }
    }

    @Override
    public void close() throws SQLException {
        if (connection != null && !connection.getAutoCommit()) {
            connection.close();
        }
    }
}
  • JDBC 事务实现类中,封装了获取连接、提交事务等操作,其实使用的就是 JDBC 本身提供的能力。

3.4.4 事务工厂

TransactionFactory.java

package com.lino.mybatis.transaction;

import com.lino.mybatis.session.TransactionIsolationLevel;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * @description: 事务工厂
 */
public interface TransactionFactory {

    /**
     * 根据 Connection 创建 事务
     *
     * @param conn 连接
     * @return 事务对象
     */
    Transaction newTransaction(Connection conn);

    /**
     * 根据数据源和事务隔离级别创建事务
     *
     * @param dataSource 数据源
     * @param level      事务隔离级别
     * @param autoCommit 是否自动提交
     * @return 事务
     */
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}
  • 以工厂方法模式包装 JDBC 事务实现,为每一个事务实现都提供一个对应的工厂。
  • 与简单工厂的接口包装不同。

3.4.5 JDBC事务工厂

JdbcTransactionFactory.java

package com.lino.mybatis.transaction.jdbc;

import com.lino.mybatis.session.TransactionIsolationLevel;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.TransactionFactory;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * @description: JDBC 事务工厂
 */
public class JdbcTransactionFactory implements TransactionFactory {

    @Override
    public Transaction newTransaction(Connection conn) {
        return new JdbcTransaction(conn);
    }

    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new JdbcTransaction(dataSource, level, autoCommit);
    }
}

3.5 数据源工厂创建

3.5.1 数据源工厂

DataSourceFactory.java

package com.lino.mybatis.datasource;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @description: 数据源工厂
 */
public interface DataSourceFactory {

    /**
     * 添加数据源
     *
     * @param props 数据源信息
     */
    void setProperties(Properties props);

    /**
     * 获取数据源
     *
     * @return 数据源
     */
    DataSource getDataSource();
}

3.5.2 阿里的druid数据库连接池

DruidDataSourceFactory.java

package com.lino.mybatis.datasource.druid;

import com.alibaba.druid.pool.DruidDataSource;
import com.lino.mybatis.datasource.DataSourceFactory;
import javax.sql.DataSource;
import java.util.Properties;

/**
 * @description: Druid 数据源工厂
 */
public class DruidDataSourceFactory implements DataSourceFactory {

    private Properties props;

    @Override
    public void setProperties(Properties props) {
        this.props = props;
    }

    @Override
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(props.getProperty("driver"));
        dataSource.setUrl(props.getProperty("url"));
        dataSource.setUsername(props.getProperty("username"));
        dataSource.setPassword(props.getProperty("password"));
        return dataSource;
    }
}

3.6 创建配置环境基础类

3.6.1 绑定的SQL对象

BoundSql.java

package com.lino.mybatis.mapping;

import java.util.Map;

/**
 * @description: 绑定的SQL,是从SqlSource而来,将动态内容都处理完成得到的SQL语句字符串,其中包括?,还有绑定的参数
 */
public class BoundSql {

    private String sql;
    private Map<Integer, String> parameterMappings;
    private String parameterType;
    private String resultType;

    public BoundSql(String sql, Map<Integer, String> parameterMappings, String parameterType, String resultType) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.parameterType = parameterType;
        this.resultType = resultType;
    }

    public String getSql() {
        return sql;
    }

    public Map<Integer, String> getParameterMappings() {
        return parameterMappings;
    }

    public String getParameterType() {
        return parameterType;
    }

    public String getResultType() {
        return resultType;
    }
}

3.6.2 参数映射对象

ParameterMapping.java

package com.lino.mybatis.mapping;

import cn.hutool.db.meta.JdbcType;
import com.lino.mybatis.session.Configuration;

/**
 * @description: 参数映射 #{property,javaType=int,jdbcType=NUMERIC}
 */
public class ParameterMapping {

    private Configuration configuration;
    /**
     * property
     */
    private String property;
    /**
     * javaType = int
     */
    private Class<?> javaType = Object.class;
    /**
     * javaType = NUMERIC
     */
    private JdbcType jdbcType;

    public ParameterMapping() {
    }

    public static class Builder {

        private ParameterMapping parameterMapping = new ParameterMapping();

        private Builder(Configuration configuration, String property) {
            parameterMapping.configuration = configuration;
            parameterMapping.property = property;
        }

        public Builder javaType(Class<?> javaType) {
            parameterMapping.javaType = javaType;
            return this;
        }

        public Builder jdbcType(JdbcType jdbcType) {
            parameterMapping.jdbcType = jdbcType;
            return this;
        }
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public String getProperty() {
        return property;
    }

    public Class<?> getJavaType() {
        return javaType;
    }

    public JdbcType getJdbcType() {
        return jdbcType;
    }
}

3.6.3 环境配置类

Environment.java

package com.lino.mybatis.mapping;

import com.lino.mybatis.transaction.TransactionFactory;
import javax.sql.DataSource;

/**
 * @description: 环境
 */
public final class Environment {

    /**
     * 环境id
     */
    private final String id;
    /**
     * 事务工厂
     */
    private final TransactionFactory transactionFactory;
    /**
     * 数据源
     */
    private final DataSource dataSource;

    public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
        this.id = id;
        this.transactionFactory = transactionFactory;
        this.dataSource = dataSource;
    }

    public static class Builder {

        private String id;
        private TransactionFactory transactionFactory;
        private DataSource dataSource;

        public Builder(String id) {
            this.id = id;
        }

        public Builder transactionFactory(TransactionFactory transactionFactory) {
            this.transactionFactory = transactionFactory;
            return this;
        }

        public Builder dataSource(DataSource dataSource) {
            this.dataSource = dataSource;
            return this;
        }

        public String id() {
            return this.id;
        }

        public Environment build() {
            return new Environment(this.id, this.transactionFactory, this.dataSource);
        }
    }

    public String getId() {
        return id;
    }

    public TransactionFactory getTransactionFactory() {
        return transactionFactory;
    }

    public DataSource getDataSource() {
        return dataSource;
    }
}

3.6.4 映射器语句类

MappedStatement.java

package com.lino.mybatis.mapping;

import com.lino.mybatis.session.Configuration;
import java.util.Map;

/**
 * @description: 映射器语句类
 */
public class MappedStatement {

    private Configuration configuration;
    private String id;
    private SqlCommandType sqlCommandType;
    private BoundSql boundSql;

    public MappedStatement() {
    }

    public static class Builder {

        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration, String id, SqlCommandType sqlCommandType, BoundSql boundSql) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlCommandType = sqlCommandType;
            mappedStatement.boundSql = boundSql;
        }

        public MappedStatement build() {
            assert mappedStatement.configuration != null;
            assert mappedStatement.id != null;
            return mappedStatement;
        }
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public String getId() {
        return id;
    }

    public SqlCommandType getSqlCommandType() {
        return sqlCommandType;
    }

    public BoundSql getBoundSql() {
        return boundSql;
    }
}
  • 去除 sql 多个参数字段,添加 BoundSql SQL 对象

3.7 类型别名注册器

Mybatis 框架中我们所需要的基本类型、数组类型以及自定定义的事务实现和事务工厂都需要注册到类型别名注册器中进行管理。
在我们需要使用的时候可以从注册器中获取到具体的对象类型,之后再进行实例化的方式进行使用。

3.7.1 JDBC枚举类型

JdbcType.java

package com.lino.mybatis.type;

import java.sql.Types;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: JDBC枚举类型
 */
public enum JdbcType {

    // JDBC枚举类型
    INTEGER(Types.INTEGER),
    FLOAT(Types.FLOAT),
    DOUBLE(Types.DOUBLE),
    DECIMAL(Types.DECIMAL),
    VARCHAR(Types.VARCHAR),
    TIMESTAMP(Types.TIMESTAMP);

    public final int TYPE_CODE;
    private static Map<Integer, JdbcType> codeLookup = new HashMap<>();

    static {
        for (JdbcType type : JdbcType.values()) {
            codeLookup.put(type.TYPE_CODE, type);
        }
    }

    JdbcType(int code) {
        this.TYPE_CODE = code;
    }

    public static JdbcType forCode(int code) {
        return codeLookup.get(code);
    }
}

3.7.2 类型别名注册机

TypeAliasRegistry.java

package com.lino.mybatis.type;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * @description: 类型别名注册机
 */
public class TypeAliasRegistry {

    private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();

    public TypeAliasRegistry() {
        // 构造函数里注册系统内置的类型别名
        registerAlias("string", String.class);

        // 基本包装类型
        registerAlias("byte", Byte.class);
        registerAlias("long", Long.class);
        registerAlias("short", Short.class);
        registerAlias("int", Integer.class);
        registerAlias("integer", Integer.class);
        registerAlias("double", Double.class);
        registerAlias("float", Float.class);
        registerAlias("boolean", Boolean.class);
    }

    public void registerAlias(String alias, Class<?> value) {
        String key = alias.toLowerCase(Locale.ENGLISH);
        TYPE_ALIASES.put(key, value);
    }

    public <T> Class<T> resolveAlias(String string) {
        String key = string.toLowerCase(Locale.ENGLISH);
        return (Class<T>) TYPE_ALIASES.get(key);
    }
}
  • TypeAliasRegistry 类型别名注册器中先做一些基本的类型注册,以及提供 registerAlias 注册方法和 resolveAlias 获取方法。

3.7.3 配置项

Configuration.java

package com.lino.mybatis.session;

import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.transaction.jdbc.JdbcTransaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;

import java.util.HashMap;
import java.util.Map;

/**
 * @description: 配置项
 * @author: lingjian
 * @createDate: 2022/11/5 16:27
 */
public class Configuration {

    /**
     * 环境
     */
    protected Environment environment;

    /**
     * 映射注册机
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    /**
     * 映射的语句,存在Map里
     */
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>(16);

    /**
     * 类型别名注册机
     */
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
    }

    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

    public boolean hasMapper(Class<?> type) {
        return mapperRegistry.hasMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }

    public MappedStatement getMappedStatement(String id) {
        return mappedStatements.get(id);
    }

    public TypeAliasRegistry getTypeAliasRegistry() {
        return typeAliasRegistry;
    }

    public Environment getEnvironment() {
        return environment;
    }

    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}
  • 添加 Environment 配置环境
  • 添加 TypeAliasRegistry 类型别名注册机,添加初始化添加 JDBC 事务工厂和 DRUID 数据源工厂
  • Configuration 配置选项中,添加类型别名注册机,通过构造函数添加 JDBCDRUID 注册操作。
  • 整个 Mybatis 的操作都是使用 Configuration 配置项进行串联流程,所有的内容都会在 Configuration 中进行连接。

3.8 解析数据源配置

通过在 XML 解析器 XMLConfigBuilder 中,扩展对环境信息的解析。这里把数据源、事务类内容成为操作 SQL 环境。
解析后把配置信息写入到 Configuration 配置项中,便于后续使用。

3.8.1 构建器基类

BaseBuilder.java

package com.lino.mybatis.builder;

import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.TypeAliasRegistry;

/**
 * @description: 构建器的基类,建造者模式
 */
public class BaseBuilder {

    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    }

    public Configuration getConfiguration() {
        return configuration;
    }
}
  • 添加 TypeAliasRegistry 类型别名注册机

3.8.2 XML配置构建器

XMLConfigBuilder.java

package com.lino.mybatis.builder.xml;

import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.datasource.DataSourceFactory;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.transaction.TransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;
import javax.sql.DataSource;
import java.io.Reader;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description: XML配置构建器,建造者模式,集成BaseBuilder
 */
public class XMLConfigBuilder extends BaseBuilder {

    private Element root;
    private static final Pattern pattern = Pattern.compile("(#\\{(.*?)})");

    public XMLConfigBuilder(Reader reader) {
        // 1.调用父类初始化Configuration
        super(new Configuration());
        // 2.dom4j 处理xml
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(new InputSource(reader));
            root = document.getRootElement();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解析配置:类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器
     *
     * @return Configuration
     */
    public Configuration parse() {
        try {
            // 环境
            environmentsElement(root.element("environments"));
            // 解析映射器
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }

    private void environmentsElement(Element context) throws Exception {
        String environment = context.attributeValue("default");

        List<Element> environmentList = context.elements("environment");
        for (Element e : environmentList) {
            String id = e.attributeValue("id");
            if (environment.equals(id)) {
                // 事务管理器
                TransactionFactory txFactory = (TransactionFactory) typeAliasRegistry.resolveAlias(e.element("transactionManager").attributeValue("type")).newInstance();

                // 数据源
                Element dataSourceElement = e.element("dataSource");
                DataSourceFactory dataSourceFactory = (DataSourceFactory) typeAliasRegistry.resolveAlias(dataSourceElement.attributeValue("type")).newInstance();
                List<Element> propertyList = dataSourceElement.elements("property");
                Properties props = new Properties();
                for (Element property : propertyList) {
                    props.setProperty(property.attributeValue("name"), property.attributeValue("value"));
                }
                dataSourceFactory.setProperties(props);
                DataSource dataSource = dataSourceFactory.getDataSource();

                // 构建环境
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                        .transactionFactory(txFactory)
                        .dataSource(dataSource);

                configuration.setEnvironment(environmentBuilder.build());
            }

        }
    }

    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            Element root = document.getRootElement();
            // 命名空间
            String namespace = root.attributeValue("namespace");

            // SELECT
            List<Element> selectNodes = root.elements("select");
            for (Element node : selectNodes) {
                String id = node.attributeValue("id");
                String parameterType = node.attributeValue("parameterType");
                String resultType = node.attributeValue("resultType");
                String sql = node.getText();

                // ? 匹配
                Map<Integer, String> parameter = new HashMap<>(16);
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) {
                    String g1 = matcher.group(1);
                    String g2 = matcher.group(2);
                    parameter.put(i, g2);
                    sql = sql.replace(g1, "?");
                }

                String msId = namespace + "." + id;
                String nodeName = node.getName();
                SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
                // SQL语句初识化
                BoundSql boundSql = new BoundSql(sql, parameter, parameterType, resultType);
                MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType, boundSql).build();
                // 添加解析SQL
                configuration.addMappedStatement(mappedStatement);
            }

            // 注册Mapper映射器
            configuration.addMapper(Resources.classForName(namespace));
        }

    }
}
  • 添加 environmentsElement 配置环境处理
  • 修改 mapperElement 中创建 MappedStatement 映射器语句类的初始化
  • XMLConfigBuilder#parse 解析扩展对数据源解析操作, 在 environmentsElement 方法中包括事务管理器解析和从注册器中读取到事务工程的实现类,同理数据源也是从类型注册器中获取。
  • 最后把事务管理器和数据源的处理,通过环境构建 Environment.Builder 存放到 Configuration 配置项中,也就可以通过 Configuration 存在的地方都可以获取到数据源了。

3.9 SQL执行和结果封装

3.9.1 默认sqlSession实现类

DefaultSqlSession.java

package com.lino.mybatis.session.defaults;

import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.SqlSession;
import java.lang.reflect.Method;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * @description: 默认sqlSession实现类
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public <T> T selectOne(String statement) {
        return (T) ("你被代理了!" + statement);
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        try {
            MappedStatement mappedStatement = configuration.getMappedStatement(statement);
            Environment environment = configuration.getEnvironment();

            Connection connection = environment.getDataSource().getConnection();

            BoundSql boundSql = mappedStatement.getBoundSql();
            PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
            preparedStatement.setLong(1, Long.parseLong(((Object[]) parameter)[0].toString()));
            ResultSet resultSet = preparedStatement.executeQuery();

            List<T> objList = resultSet2Obj(resultSet, Class.forName(boundSql.getResultType()));
            return objList.get(0);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
        List<T> list = new ArrayList<>();
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 每次遍历值
            while (resultSet.next()) {
                T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                    Method method;
                    if (value instanceof Timestamp) {
                        method = clazz.getMethod(setMethod, LocalDateTime.class);
                    } else {
                        method = clazz.getMethod(setMethod, value.getClass());
                    }
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.getMapper(type, this);
    }

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }
}
  • 修改 selectOne,引入配置环境和数据源,将之前打印改为调用 JDBC 连接数据查询 SQL
  • 添加 resultSet2Obj 返回结果处理方法
  • selectOne 方法中获取 Connection 数据源连接,并简单的执行 SQL 语句,并对执行的结果进行封装处理。

四、测试:数据源的解析、创建和使用

4.1 创建 mybatis 数据库并添加数据库表

CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '用户ID',
        userHead VARCHAR(16) COMMENT '用户头像',
        createTime TIMESTAMP NULL COMMENT '创建时间',
        updateTime TIMESTAMP NULL COMMENT '更新时间',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-11-07 00:00:00', '2022-11-07 00:00:00', '小零');
  • 创建一个数据库名为 mybatis 的数据库
  • 在数据库 mybatis 中创建表 user,并添加测试数据

4.2 提供 DAO 接口 和 User 实体类

4.2.1 用户持久层

IUserDao.java

package com.lino.mybatis.test.dao;

import com.lino.mybatis.test.po.User;

/**
 * @Description: 用户持久层
 */
public interface IUserDao {

    /**
     * 根据ID查询用户信息
     *
     * @param uId ID
     * @return String 名称
     */
    User queryUserInfoById(Long uId);
}
  • 返回结果改为 User 实体类

4.2.2 用户类

User.java

package com.lino.mybatis.test.po;

import cn.hutool.core.date.DateTime;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * @description: 用户实例类
 */
public class User {

    private Long id;
    /**
     * 用户ID
     */
    private String userId;
    /**
     * 头像
     */
    private String userHead;
    /**
     * 用户名称
     */
    private String userName;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;

    public Long getId() {
        return id;
    }

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

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserHead() {
        return userHead;
    }

    public void setUserHead(String userHead) {
        this.userHead = userHead;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
}
  • 添加 userName 属性

4.3 配置数据源和配置Mapper

4.3.1 配置文件

mybatis-config-datasource.xml


DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="DRUID">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            dataSource>
        environment>
    environments>
    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    mappers>
configuration>
  • 添加 environments 数据库配置信息。
  • 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
  • DataSource 配置的是 DRUID,目前只实现了这个数据源。

4.3.2 用户接口配置文件

User_Mapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lino.mybatis.test.dao.IUserDao">
    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="com.lino.mybatis.test.po.User">
        SELECT id, userId, userHead, userName
        FROM user
        WHERE id = #{id}
    select>
mapper>
  • 去除 createTime 查询字段, 添加 userName 查询字段。

4.4 单元测试

ApiTest

/**
 * 测试映射器注册机
 */
@Test
public void test_SqlSessionFactory() throws IOException {
    // 1.从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 2.获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 3.测试验证
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试结果:{}", JSON.toJSONString(user));
}

测试结果

08:32:18.875 [main] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
08:32:19.623 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小零哥"}
  • 从测试结果看,通过对数据源的解析、包装和使用,已经可以对 SQL 语句进行执行和包装返回的结果信息了。

4.5 功能验证

ApiTest

@Test
public void test_selectOne() throws IOException {
    // 解析XML
    Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
    XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
    Configuration configuration = xmlConfigBuilder.parse();

    // 获取 DefaultSqlSession
    SqlSession sqlSession = new DefaultSqlSession(configuration);

    // 执行查询:默认是一个集合参数
    Object[] req = {1L};
    Object result = sqlSession.selectOne("com.lino.mybatis.test.dao.IUserDao.queryUserInfoById", req);
    logger.info("测试结果:{}", JSON.toJSONString(result));
}
  • 对本章节新增的内容进行提取,进行测试验证。
  • 新增内容:解析内容的添加、处理 XML 配置中的数据源信息,以及解析后可以在 DefaultSqlSession 中调用数据源执行 SQL 语句并返回结果。

测试结果

10:19:43.519 [main] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
10:19:44.306 [main] INFO  com.lino.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小灵哥"}
  • 测试结果是通过的。
  • XML 数据源元素配置的解析,到 Configuration 资源的注册以及写入相关配置到配置项,并在 DefaultSqlSession 中进行使用。
  • 同时这里跳过代理方式获取 Mapper 而是直接拿到 SqlSession 执行 selectOne 方法的方式进行处理,这样更容易观察整个功能的迭代开发。

手写Mybatis:第5章-数据源的解析、创建和使用_第3张图片

五、总结:数据源的解析、创建和使用

  • 以解析 XML 配置解析为入口,添加数据源的整合和包装,引出事务工厂对 JDBC 事务的处理,并加载到环境配置中进行运用。
  • 通过数据源的引入就可以在 DefaultSqlSession 中从 Configuration 配置引入环境信息,把对应的 SQL 语句提交给 JDBC 进行处理并简单封装结果数据。

你可能感兴趣的:(手写mybatis,mybatis,java)