《手写Mybatis渐进式源码实践》实践笔记(第四章 Mapper XML的解析和注册使用)


文章目录


第4章 Mapper XML的解析和注册使用

![mybatis](https://raw.githubusercontent.com/swg209/my_img/main/d7410afe519f55aeabfcc21f57148bc4.jpeg

背景

技术背景

建造者模式(Builder Pattern)是一种创建型设计模式,它提供了一种分步骤构建复杂对象的方法。这种模式允许你通过指定复杂对象的类型和内容,逐步构建这些对象。

建造者模式的主要目的是将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式的主要角色:
  1. 产品(Product):需要构建的复杂对象。
  2. 抽象建造者(Builder):创建一个包含创建产品的方法的接口。
  3. 具体建造者(Concrete Builder):实现抽象建造者接口,并提供方法以构造和返回产品。
  4. 指挥者(Director):包含一个构建方法,该方法接受一个建造者对象,并指导它构建产品。
  5. 客户端(Client):创建一个具体建造者对象,通过指挥者来构建产品。
建造者模式的步骤:
  1. 定义产品:定义一个复杂的对象,这个对象将由建造者模式构建。
  2. 创建抽象建造者:定义一个接口,声明构建复杂对象的步骤。
  3. 实现具体建造者:实现抽象建造者接口,提供具体的构建步骤实现。
  4. 创建指挥者:包含一个方法,接受一个建造者对象,并指导它构建产品。
  5. 使用建造者模式:客户端代码创建一个具体建造者对象,并使用指挥者来构建产品。
建造者模式的优点:
  • 灵活性:客户端不必知道产品内部的构建细节。
  • 隔离复杂性:客户端代码与产品构建代码分离。
  • 扩展性:可以添加新的建造者,以创建新的产品变体,而不影响现有代码。
建造者模式的缺点:
  • 类的膨胀:需要为每种产品变体创建一个建造者类。
  • 过度使用:对于简单的对象,使用建造者模式可能会过度设计。
建造者模式的适用场景:
  • 当创建复杂对象的算法应该独立于创建该对象的类时。
  • 当构造过程需要被客户独立出来,或者当构造过程需要被分离成几个不同的步骤,而每个步骤可以独立于其他步骤进行时。
示例代码(Java):
java 复制代码
// 产品类
public class Computer {
    private String cpu;
    private String gpu;
    private String ram;

    // 省略getter和setter方法
}

// 抽象建造者
public interface ComputerBuilder {
    void buildCPU();
    void buildGPU();
    void buildRAM();
    Computer getComputer();
}

// 具体建造者
public class ConcreteBuilder implements ComputerBuilder {
    private Computer computer = new Computer();

    @Override
    public void buildCPU() {
        computer.setCpu("Intel i7");
    }

    @Override
    public void buildGPU() {
        computer.setGpu("NVIDIA RTX 3080");
    }

    @Override
    public void buildRAM() {
        computer.setRam("16GB");
    }

    @Override
    public Computer getComputer() {
        return computer;
    }
}

// 指挥者
public class ComputerDirector {
    private ComputerBuilder builder;

    public ComputerDirector(ComputerBuilder builder) {
        this.builder = builder;
    }

    public Computer construct() {
        builder.buildCPU();
        builder.buildGPU();
        builder.buildRAM();
        return builder.getComputer();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        ComputerBuilder builder = new ConcreteBuilder();
        ComputerDirector director = new ComputerDirector(builder);
        Computer computer = director.construct();
        // 使用computer对象
    }
}

在这个例子中,Computer 是产品,ComputerBuilder 是抽象建造者,ConcreteBuilder 是具体建造者,ComputerDirector 是指挥者,客户端代码创建了一个具体建造者并通过指挥者构建了产品。

业务背景

在我们渐进式的逐步实现 Mybatis 框架过程中,要把握一条目标导向的演进思路,才能指导我们下一步做什么,这个演进思路就是 Mybatis 的核心逻辑怎么实现的。

为了便于理解,我们可以将一个 ORM 框架的目标,简单的描述成是为了给一个接口提供代理类,类中包括了对 Mapper 也就是 xml 文件中的 SQL 信息(类型入参出参条件)进行解析,然后处理,这个处理过程就是对数据库的操作以及返回对应的结果给到接口。

ORM 框架核心流程如图:

那么按照 ORM 核心流程的执行过程,我们就需要在上一章节的基础上,继续扩展对 Mapper 文件的解析以及提取出对应的 SQL 文件。并在当前这个阶段,可以满足我们调用 DAO 接口方法的时候,可以返回 Mapper 中对应的待执行 SQL 语句。

目标

实现从配置的XML文件中,解析Mapper文件,提取出对应的SQL,并在Dao执行接口方法时,输出执行的SQL内容。

设计

上一章节我们使用了 MapperRegistry 对包路径进行扫描注册映射器,并在 DefaultSqlSession 中进行使用。本章节将命名空间、SQL描述、映射信息统一维护到每一个 DAO 对应的 Mapper XML 的文件,其实 XML文件就是我们的源头了。通过对 XML 文件的解析和处理就可以完成 Mapper 映射器的注册和 SQL 管理。这样能更方便操作和使用框架。

整体设计如图 :

  • 定义 SqlSessionFactoryBuilder 工厂建造者模式类,通过入口 IO 的方式对 XML 文件进行解析。当前我们主要以解析 SQL 部分为主,并注册映射器,串联出整个核心流程的脉络。
  • 文件解析以后会存放到 Configuration 配置类中,这个配置类会被串联到整个 Mybatis 流程中,所有内容存放和读取都离不开这个类。比如我们在 DefaultSqlSession 中获取 Mapper 和执行 selectOne 也同样是需要从 Configuration 配置类中读取相关信息操作。

实现

工程代码

类图

  • SqlSessionFactoryBuilder 作为整个 Mybatis 的入口,提供建造者工厂,包装 XML 解析处理,并返回对应 SqlSessionFactory 处理类。
  • 通过解析把 XML 信息注册到 Configuration 配置类中,再通过传递 Configuration 配置类到各个逻辑处理类里,包括 DefaultSqlSession 中,这样就可以在获取映射器和执行SQL的时候,从配置类中拿到对应的内容了。

实现步骤

1.构建SqlSessionFactory建造者工厂

SqlSessionFactoryBuilder

  • SqlSessionFactoryBuilder 是整个 Mybatis 的入口类,通过指定解析XML的IO,引导整个流程的启动。
  • 从这个类开始新增加了 XMLConfigBuilder、Configuration 两个处理类,分别用于解析 XML 和串联整个流程的对象保存操作。
java 复制代码
public class SqlSessionFactoryBuilder {


    public SqlSessionFactory build(Reader reader) {
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);
        return build(xmlConfigBuilder.parse());
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }


}
2.XML 解析处理

XMLConfigBuilder

  • XMLConfigBuilder 核心操作在于初始化 Configuration,从 XML解析获得配置信息。
  • parse() 解析操作,并把解析后的信息,通过 Configuration 配置类进行存放,包括:添加解析 SQL、注册Mapper映射器。
  • 解析配置整体包括:类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型转换、映射器,但目前我们还不需要那么多,所以只做一些必要的 SQL 解析处理。
java 复制代码
public class XMLConfigBuilder extends BaseBuilder {

    private Element root;

    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();
        }
    }


    //从xml文件解析Configuration.
    public Configuration parse() {
        try {
            // 解析映射器,读取XML文件中的mappers标识.
            mapperElement(root.element("mappers"));
        } catch (Exception e) {
            throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
        return configuration;
    }


    /**
     * XMLmapper 格式.
     * <mappers>
     * <mapper resource="mapper/User_Mapper.xml"/>
     * </mappers>
     *
     * @param mappers
     * @throws Exception
     */
    private void mapperElement(Element mappers) throws Exception {
        //获取mappers标签下的所有mapper标签.
        List<Element> mapperList = mappers.elements("mapper");
        for (Element mapper : mapperList) {
            //获取mapper标签的resource属性.
            String resource = mapper.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> selectList = root.elements("select");
            for (Element node : selectList) {
                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<>();
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                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));
                MappedStatement mappedStatement = new MappedStatement.Builder(configuration, msId, sqlCommandType,
                        parameterType, resultType, sql, parameter).build();
                // 添加解析SQL
                configuration.addMappedStatement(mappedStatement);
            }

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


}
3. 通过配置类包装注册机和SQL语句

Configuration

  • 在配置类中添加映射器注册机MapperRegistry和映射语句MappedStatement的存放;

  • 映射器注册机,用于注册 Mapper 映射器锁提供的操作类。

  • 另外一个 MappedStatement 是本章节新添加的 SQL 信息记录对象,包括记录:SQL类型、SQL语句、入参类型、出参类型等。

java 复制代码
public class Configuration {

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

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


    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) {
        mappedStatementMap.put(ms.getId(), ms);
    }

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


}
4. DefaultSqlSession结合配置项获取信息
  • DefaultSqlSession 相对于上一章节,这里把 MapperRegistry mapperRegistry 替换为 Configuration configuration,这样才能传递更丰富的信息内容,而不只是注册器操作。
  • 之后在 DefaultSqlSession#selectOne、DefaultSqlSession#getMapper 两个方法中都使用 configuration 来获取对应的信息。
  • 目前 selectOne 方法中只是把获取的信息进行打印,后续将引入 SQL 执行器进行结果查询并返回。
java 复制代码
public class DefaultSqlSession implements SqlSession {

    /**
     * 配置项.
     */
    private Configuration configuration;

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

    public Configuration getConfiguration() {
        return configuration;
    }


    /**
     * 根据给定的执行SQL获取一条记录的封装对象.
     *
     * @param statement
     * @param <T>
     * @return
     */

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

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement mappedStatement = configuration.getMappedStatement(statement);
        return (T) ("你的操作被代理了!" + "方法:" + statement + " 入参:" + parameter + "\n待执行SQL:" + mappedStatement.getSql());

    }

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


}

测试

事先准备

定义一个数据库接口 IUserDao

IUserDao

java 复制代码
public interface IUserDao {

    String queryUserInfoById(String uid);

}

定义对应的mapper xml文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.suwg.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.suwg.mybatis.test.po.User">
        SELECT id, user_id, user_head, create_time
        FROM user
        where id = #{id}
    </select>

</mapper>

测试用例

  • 在单元测试中,实现的步骤为:
    1. 从XML文件读取配置项,通过SqlSessionFactoryBuilder获取到SqlSessionFactory
    2. 从SqlSessionFactory获取SqlSession
    3. 获取映射器对象
    4. 调用Dao方法
java 复制代码
public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);


    // 测试SqlSessionFactory
    @Test
    public void testSqlSessionFactory() throws IOException {

        // 1.从xml文件读取mybatis配置项
        Reader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
         // 2.从SqlSessionFactory获取SqlSession.
        SqlSession sqlSession = sqlSessionFactory.openSession();


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

        // 4.测试验证
        String result = userDao.queryUserInfoById(10001L);
        logger.info("测试结果:{}", result);

    }


}

测试结果

从输出的结果来看,我们实现的Mybatis手写 ORM 框架中,目前的代理操作已经可以打印 XML 中解析的 SQL 信息了,后续我们将结合这部分的处理继续完成数据库的操作。

总结

  • 熟悉ORM 处理的核心流程,知晓每一章节对应核心流程的步骤,和要完成的内容,熟悉代理、封装、解析和返回结果的过程,后续才能更好的完成整个框架的实现。
  • SqlSessionFactoryBuilder 的引入包装了整个执行过程,包括:XML 文件的解析、Configuration 配置类的处理,让 DefaultSqlSession 可以更加灵活的拿到对应的信息,获取 Mapper 和 SQL 语句。
  • 另外从整个工程搭建的过程中,可以看到有很多工厂模式、建造者模式、代理模式的使用,这些设计模式技巧都可以让整个工程变得易于维护和易于迭代。

参考书籍:《手写Mybatis渐进式源码实践》

书籍源代码:https://github.com/fuzhengwei/book-small-mybatis

相关推荐
还是鼠鼠1 分钟前
SQL语句执行很慢,如何分析呢?
java·数据库·mysql·面试
云和数据.ChenGuang9 分钟前
批量给100台服务器装系统,还要完成后续的配置和软件部署
运维·服务器·开发语言·mysql
锥锋骚年13 分钟前
golang 发送内网邮件和外网邮件
开发语言·后端·golang
雨雨雨雨雨别下啦25 分钟前
Spring AOP概念
java·后端·spring
on the way 12325 分钟前
day04-Spring之Bean的生命周期
java·后端·spring
阿蒙Amon27 分钟前
JavaScript学习笔记:14.类型数组
javascript·笔记·学习
代码笔耕27 分钟前
面向对象开发实践之消息中心设计(二)
java·后端·架构
云水木石35 分钟前
Rust 语言开发的 Linux 桌面来了
linux·运维·开发语言·后端·rust
XFF不秃头36 分钟前
力扣刷题笔记-下一个排列
c++·笔记·算法·leetcode
Lv117700837 分钟前
Visual Studio中Array数组的常用查询方法
笔记·算法·c#·visual studio