当前位置: 首页 > article >正文

【从零开发Mybatis】构建SQL映射语句对应的MappedStatement对象

引言

在上文【从零开发Mybatis】引入Configuration\XMLConfigBuilder\SqlSessionFactoryBuilder\DefaultSqlSessionFactory等关键类中,我们通过定义Configuration、XMLConfigBuilder、SqlSessionFactoryBuilder 和 DefaultSqlSessionFactory 类,使我们的MyBatis 框架变得更加模块化和易于维护。Configuration 对象作为全局配置信息的容器,XMLConfigBuilder 负责配置信息的解析,SqlSessionFactory 提供了创建 SqlSession 的工厂方法,而 SqlSessionFactoryBuilder 则负责创建 SqlSessionFactory 实例。这些组件协同工作,使得 MyBatis 能够有效地管理数据库操作。

到目前为止,我们的简易版Mybatis框架仍然存在一个关键步骤没有实现,就是构建SQL映射语句对应的MappedStatement对象,在上一个版本中,我们仅仅是构建了一条静态SQL语句,存在我们的全局Configuration 对象中。

接下来,我们将引入XMLMapperBuilder\XMLStatementBuilder\MapperBuilderAssistant\MappedStatement对象,用于从 XML 配置文件中解析映射语句(mapped statements),并将这些映射语句转成MappedStatement对象注册到 Configuration 对象中。

定义MappedStatement类

以下MappedStatement 类是一个用于描述 SQL 映射语句的元数据和配置信息的类。在 MyBatis 中,每一个 SQL 映射语句都有一个唯一的 MappedStatement 对象与之对应。

MappedStatement 类在 MyBatis 框架中扮演了重要的角色,它描述了 SQL 映射语句的元数据和配置信息。通过 Builder 类,可以在创建 MappedStatement 实例时指定必要的属性。这些实例在执行 SQL 操作时会被 SqlSession 使用,从而确保 SQL 映射语句的正确执行。


package org.apache.ibatis.mapping;


import org.apache.ibatis.session.Configuration;

/**
 * 用于描述一个 SQL 映射语句的元数据和配置信息。在 MyBatis 中,每一个 SQL映射语句都有一个唯一的 MappedStatement 对象与之对应
 *
 * @author crazy coder
 * @since 2024/10/19
 **/
public final class MappedStatement {
    private Configuration configuration;
    private String resource;
    private String id;
    private String sqlSource;

    MappedStatement() {
        // constructor disabled
    }

    public static class Builder {
        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration, String id, String sqlSource, String resource) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlSource = sqlSource;
            mappedStatement.resource = resource;
        }

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

    public String getResource() {
        return resource;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public String getId() {
        return id;
    }

    public String getSqlSource() {
        return sqlSource;
    }
}

以下是关于MappedStatement 类的详细说明:

类成员变量
  1. configuration

    • 类型:Configuration
    • 用途:存储全局配置信息,包括数据库连接信息、事务管理策略、SQL 映射等。
  2. resource

    • 类型:String
    • 用途:记录映射语句所在的资源文件名,如 XML 文件名。
  3. id

    • 类型:String
    • 用途:映射语句的唯一标识符,由命名空间(通常是 Mapper 接口的全限定名)加上 SQL 语句的 ID 组成。
  4. sqlSource

    • 类型:String
    • 用途:SQL 语句的源代码,即实际执行的 SQL 语句文本。
构造函数

由于 MappedStatement 类的默认构造函数被禁用了(MappedStatement()),因此需要通过 Builder 类来创建 MappedStatement 实例。

内部类 Builder

Builder 类提供了一种构建 MappedStatement 实例的方法,允许在创建过程中指定必要的属性。

方法
  • Builder(Configuration configuration, String id, String sqlSource, String resource)

    • 参数:
      • configurationConfiguration 对象,包含全局配置信息。
      • id:映射语句的唯一标识符。
      • sqlSource:SQL 语句的源代码。
      • resource:映射语句所在的资源文件名。
    • 作用:初始化 Builder 实例,并设置 MappedStatement 的属性。
  • build()

    • 返回值:MappedStatement 对象。
    • 作用:创建并返回 MappedStatement 实例。在返回之前,会断言 configurationidsqlSource 不为空。
Getter 方法

这些方法提供了对 MappedStatement 类成员变量的访问接口,允许外部代码获取这些配置信息。

定义XMLMapperBuilder类

以下 XMLMapperBuilder 类用于处理 MyBatis 中的 Mapper XML 文件,并将映射信息注册到 Configuration 对象中。


package org.apache.ibatis.builder.xml;

import java.io.Reader;

import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.parsing.BuilderException;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;


/**
 * 用于解析 Mapper XML 文件,并将映射信息注册到 Configuration 对象中
 *
 * @author crazy coder
 * @since 2024/10/19
 **/
public class XMLMapperBuilder {
    private final XPathParser parser;
    private final MapperBuilderAssistant builderAssistant;
    private final String resource;


    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource) {
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
        this.parser = new XPathParser(reader);
        this.resource = resource;
    }


    public void parse() {
        configurationElement(parser.evalNode("/mapper"));
    }

    private void configurationElement(XNode context) {
        try {
            String namespace = context.getAttributes().getProperty("namespace");
            if (namespace == null || namespace.isEmpty()) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            builderAssistant.setCurrentNamespace(namespace);
            buildStatementFromContext(context.evalNode("select"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }


    private void buildStatementFromContext(XNode context) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(builderAssistant, context);
        statementParser.parseStatementNode();
    }


}

下面是对这个类的详细说明:

成员变量

    private final XPathParser parser;
    private final MapperBuilderAssistant builderAssistant;
    private final String resource;
  1. parser

    • 类型:XPathParser
    • 用途:用于从 XML 文档中解析 XPath 表达式。
  2. builderAssistant

    • 类型:MapperBuilderAssistant
    • 用途:辅助构建映射器对象,并将映射信息添加到全局配置中。
  3. resource

    • 类型:String
    • 用途:表示 XML 文件的资源位置或名称。

构造函数

    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource) {
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
        this.parser = new XPathParser(reader);
        this.resource = resource;
    }

此构造函数初始化 XMLMapperBuilder 对象,并设置其成员变量。

方法

parse()
    public void parse() {
        configurationElement(parser.evalNode("/mapper"));
    }

此方法开始解析 XML 文件的根节点 /mapper,并将结果传递给 configurationElement 方法进行进一步处理。

configurationElement(XNode context)
    private void configurationElement(XNode context) {
        try {
            String namespace = context.getAttributes().getProperty("namespace");
            if (namespace == null || namespace.isEmpty()) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            builderAssistant.setCurrentNamespace(namespace);
            buildStatementFromContext(context.evalNode("select"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }

此方法负责解析 XNode 对象中的 namespace 属性,并检查其是否为空。如果 namespace 是空的,则抛出异常。然后设置当前的命名空间,并继续解析 select 节点。

buildStatementFromContext(XNode context)
    private void buildStatementFromContext(XNode context) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(builderAssistant, context);
        statementParser.parseStatementNode();
    }

此方法创建一个新的 XMLStatementBuilder 实例,并将当前的上下文传递给它,以便于解析具体的 SQL 语句节点。

使用场景

这个类在 MyBatis 的启动过程中被使用,当读取 Mapper XML 文件时,XMLMapperBuilder 负责解析这些文件,并向 Configuration 对象注册映射信息。这使得 MyBatis 能够根据 XML 文件中的定义来动态地创建和管理 SQL 映射。

注意事项

  • 当解析 XML 文件时,必须确保 namespace 属性不为空,否则会抛出 BuilderException 异常。
  • 如果在解析过程中发生任何错误,也会抛出 BuilderException 并附带详细的错误信息。

定义XMLStatementBuilder类

XMLStatementBuilder 类用于从 XML 配置文件中解析映射语句(mapped statements),并将这些映射语句注册到 MyBatis 的 Configuration 对象中。


package org.apache.ibatis.builder.xml;

import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.parsing.XNode;

/**
 * 从 XML 配置文件中解析映射语句(mapped statements),并将这些映射语句注册到 Configuration 对象中
 *
 * @author crazy coder
 * @since 2024/10/19
 **/
public class XMLStatementBuilder {
    private final MapperBuilderAssistant builderAssistant;
    private final XNode context;

    public XMLStatementBuilder(MapperBuilderAssistant builderAssistant, XNode context) {
        this.builderAssistant = builderAssistant;
        this.context = context;
    }

    public void parseStatementNode() {
        String id = context.getAttributes().getProperty("id");
        String sqlSource = context.getBody();
        builderAssistant.addMappedStatement(id, sqlSource);
    }

}

下面是对这个类的详细说明:

成员变量

    private final MapperBuilderAssistant builderAssistant;
    private final XNode context;
  1. builderAssistant

    • 类型:MapperBuilderAssistant
    • 用途:辅助构建映射器对象,并将映射信息添加到全局配置中。
  2. context

    • 类型:XNode
    • 用途:表示 XML 配置文件中的节点,用于提取映射语句的相关信息。

构造函数

    public XMLStatementBuilder(MapperBuilderAssistant builderAssistant, XNode context) {
        this.builderAssistant = builderAssistant;
        this.context = context;
    }

此构造函数初始化 XMLStatementBuilder 对象,并设置其成员变量。

方法

parseStatementNode()
    public void parseStatementNode() {
        String id = context.getAttributes().getProperty("id");
        String sqlSource = context.getBody();
        builderAssistant.addMappedStatement(id, sqlSource);
    }

此方法负责解析 XNode 对象中的 id 属性以及节点体内的 SQL 语句内容,并调用 builderAssistantaddMappedStatement 方法来注册映射语句。

使用场景

这个类会在 MyBatis 的启动过程中被使用,当读取 Mapper XML 文件时,XMLStatementBuilder 负责解析这些文件中的映射语句节点,并向 Configuration 对象注册映射信息。这使得 MyBatis 能够根据 XML 文件中的定义来动态地创建和管理 SQL 映射。

注意事项

  • 确保 XML 文件中的每个映射语句节点都有唯一的 id 属性,以便在执行时能够正确识别每个语句。
  • 如果在解析过程中发现 id 属性缺失或其他错误,应该在 MapperBuilderAssistant 或其他相关类中处理这些情况,可能通过抛出自定义异常等方式。

定义MapperBuilderAssistant类

MapperBuilderAssistant 类主要用于将解析出的映射语句注册到 MyBatis 的 Configuration 对象中,并处理与映射语句相关的配置细节。


package org.apache.ibatis.builder;


import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.BuilderException;
import org.apache.ibatis.session.Configuration;

/**
 * 用于将解析出的映射语句注册到 Configuration 对象中,并处理与映射语句相关的一些配置细节
 *
 * @author crazy coder
 * @since 2024/10/19
 **/
public class MapperBuilderAssistant {

    private final Configuration configuration;
    private String currentNamespace;
    private final String resource;

    public MapperBuilderAssistant(Configuration configuration, String resource) {
        this.configuration = configuration;
        this.resource = resource;
    }


    public void setCurrentNamespace(String currentNamespace) {
        if (currentNamespace == null) {
            throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
        }

        if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
            throw new BuilderException("Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'.");
        }

        this.currentNamespace = currentNamespace;
    }

    public String applyCurrentNamespace(String base) {
        if (base == null) {
            return null;
        }
        return currentNamespace + "." + base;
    }

    public MappedStatement addMappedStatement(String id, String sqlSource) {
        id = applyCurrentNamespace(id);
        MappedStatement statement = new MappedStatement.Builder(configuration, id, sqlSource, resource).build();
        configuration.addMappedStatement(statement);
        return statement;
    }


}

下面是对这个类的详细说明:

成员变量

    private final Configuration configuration;
    private String currentNamespace;
    private final String resource;
  1. configuration

    • 类型:Configuration
    • 用途:存储全局配置信息,包括数据库连接信息、事务管理策略、SQL 映射等。
  2. currentNamespace

    • 类型:String
    • 用途:记录当前映射语句所属的命名空间,通常为 Mapper 接口的全限定名。
  3. resource

    • 类型:String
    • 用途:记录映射语句所在的资源文件名,如 XML 文件名。

构造函数

    public MapperBuilderAssistant(Configuration configuration, String resource) {
        this.configuration = configuration;
        this.resource = resource;
    }

此构造函数初始化 MapperBuilderAssistant 对象,并设置其成员变量。

方法

setCurrentNamespace(String currentNamespace)
    public void setCurrentNamespace(String currentNamespace) {
        if (currentNamespace == null) {
            throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
        }

        if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
            throw new BuilderException("Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'.");
        }

        this.currentNamespace = currentNamespace;
    }

此方法用于设置当前映射语句的命名空间。它会检查传入的命名空间是否为空,并且如果已有命名空间被设置,还会检查新传入的命名空间是否与现有的一致。

applyCurrentNamespace(String base)
    public String applyCurrentNamespace(String base) {
        if (base == null) {
            return null;
        }
        return currentNamespace + "." + base;
    }

此方法用于将当前命名空间与给定的基本 ID 进行拼接,形成完整的映射语句 ID。

addMappedStatement(String id, String sqlSource)
    public MappedStatement addMappedStatement(String id, String sqlSource) {
        id = applyCurrentNamespace(id);
        MappedStatement statement = new MappedStatement.Builder(configuration, id, sqlSource, resource).build();
        configuration.addMappedStatement(statement);
        return statement;
    }

此方法用于创建并注册新的 MappedStatement 对象。首先,它会使用 applyCurrentNamespace 方法来拼接完整的映射语句 ID,然后使用 MappedStatement.Builder 创建新的 MappedStatement 对象,并将其添加到全局配置对象中。

使用场景

这个类通常在 MyBatis 解析 Mapper XML 文件的过程中被使用。当读取 Mapper XML 文件时,MapperBuilderAssistant 负责将解析出的映射语句信息注册到全局配置对象中,同时处理一些必要的配置细节,例如命名空间的管理、映射语句 ID 的拼接等。

修改org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement方法

由于我们在这一版本中定义了XMLMapperBuilder类,所以原先XMLConfigBuilder类的mapperElement可以进行修改,基于XMLMapperBuilder类去实现Mapper的解析,如下所示:

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode pxNode : parent.getChildren()) {
                String resource = pxNode.getAttributes().get("resource").toString();
                try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource)) {
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(new InputStreamReader(inputStream), configuration, resource);
                    mapperParser.parse();
                }
            }
        }
    }

这段代码展示了如何处理一个 XML 配置文件中的 <mapper> 元素及其子元素。具体来说,这段方法 mapperElement 用于解析 <mapper> 元素下的子元素,特别是那些指定了外部 resource 的子元素。下面是这段代码的详细解释:

参数

  • parent:类型为 XNode,表示 XML 配置文件中的 <mapper> 元素。parent 可能包含多个子元素,这些子元素可能是指定了外部资源文件的 <mapper> 子元素。

逻辑分析

  1. 条件判断

    • 检查 parent 是否非空。如果是空,则直接返回,不执行后续操作。
  2. 遍历子元素

    • 遍历 parent 下的所有子元素(pxNode)。
  3. 获取资源路径

    • 从当前子元素 pxNode 的属性中获取 resource 属性的值。这个值通常是一个指向外部 XML 文件的路径。
  4. 加载资源文件

    • 使用当前线程的类加载器 (Thread.currentThread().getContextClassLoader()) 加载指定的资源文件。这里使用了 try-with-resources 语句来确保流在使用后被正确关闭。
    • 将输入流包装成 InputStreamReader,以支持字符编码的读取。
  5. 创建并解析 XMLMapperBuilder

    • 创建一个 XMLMapperBuilder 实例,传入包装后的输入流、全局配置对象 configuration 以及资源文件的路径 resource
    • 调用 XMLMapperBuilderparse 方法来解析并注册映射语句到全局配置对象中。

使用场景

这个方法在 MyBatis 解析主 Mapper XML 文件时被调用。当主文件包含了 <mapper> 子元素,并且这些子元素指定了外部的资源文件时,这个方法就会被用来解析这些外部文件。这种方式允许在一个主配置文件中引用多个独立的 Mapper 文件,从而方便管理和组织 SQL 映射语句。

注意事项

  • 如果 resource 属性不存在或者无法加载指定的资源文件,程序将会抛出异常。
  • 如果没有显式设置线程上下文类加载器(Thread.currentThread().setContextClassLoader()),则可能会使用默认的类加载器来查找资源文件。
  • 如果资源文件中的 XML 格式有误,XMLMapperBuilderparse 方法可能会抛出异常。

示例

假设有一个主 Mapper XML 文件 main-mapper.xml,其中包含一个引用了外部文件 external-mapper.xml<mapper> 元素:

<mapper>
    <mapper resource="external-mapper.xml"/>
</mapper>

你可以在解析主 Mapper 文件时调用 mapperElement 方法:

// 假设 mainNode 是 main-mapper.xml 文件的根节点
mapperElement(mainNode);

这段代码会找到并解析 external-mapper.xml 文件,并将其中的映射语句注册到全局配置对象中。

基于MappedStatement实现的新的Configuration类

package org.apache.ibatis.session;

import org.apache.ibatis.mapping.MappedStatement;

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

/**
 * 全局配置类
 *
 * @author crazy coder
 * @since 2024/9/27
 **/
public class Configuration {
    // 驱动
    private String driver;
    // 数据库连接 URL
    private String url;
    // 数据库用户名
    private String username;
    // 数据库密码
    private String password;

    private Map<String, MappedStatement> mappedStatements = new HashMap<>();

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    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 void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }

    public Map<String, MappedStatement> getMappedStatements() {
        return mappedStatements;
    }
}

与上一个版本的主要区别是成员变量mappedStatements 的改变,由Map<String, String>改为Map<String, MappedStatement>类型。同样在DefaultSqlSession类中,获取SQL改成从MappedStatement对象中获取。

当前版本整体项目结构

在这里插入图片描述

总结

1. XMLMapperBuilder

职责:解析 Mapper XML 文件,并将映射信息注册到 MyBatis 的 Configuration 对象中。

关键方法

  • parse(): 开始解析 XML 文件中的映射信息。
  • configurationElement(XNode context): 处理 XML 文件中的 <mapper> 元素,获取命名空间,并调用 buildStatementFromContext 来处理具体的映射语句。

2. XMLStatementBuilder

职责:从 XML 配置文件中解析映射语句(mapped statements),并将这些映射语句注册到 Configuration 对象中。

关键方法

  • parseStatementNode(): 解析 XML 文件中的映射语句节点,并调用 MapperBuilderAssistant 的方法来注册映射语句。

3. MapperBuilderAssistant

职责:辅助将解析出的映射语句注册到 Configuration 对象中,并处理与映射语句相关的配置细节。

关键方法

  • setCurrentNamespace(String currentNamespace): 设置当前映射语句的命名空间。
  • applyCurrentNamespace(String base): 将当前命名空间与基本 ID 进行拼接,形成完整的映射语句 ID。
  • addMappedStatement(String id, String sqlSource): 创建并注册新的 MappedStatement 对象。

4. MappedStatement

职责:表示一个 SQL 映射语句的对象,包含了 SQL 语句的元数据信息,如 ID、SQL 语句本身、参数映射、结果映射等。

相关操作

  • MapperBuilderAssistant 在解析映射语句时创建。
  • 注册到 Configuration 对象中,供后续查询、更新等操作使用。

总结

这些类协同工作,使得 MyBatis 能够从 XML 配置文件中解析并注册映射语句。具体流程如下:

  1. XMLMapperBuilder 负责解析整个 Mapper XML 文件,从中提取 <mapper> 元素,并处理其中的命名空间和其他属性。
  2. 在处理 <mapper> 元素时,调用 buildStatementFromContext 方法,创建 XMLStatementBuilder 实例来解析具体的映射语句。
  3. XMLStatementBuilder 解析具体的 <select>等 SQL 映射语句,并调用 MapperBuilderAssistant 的方法来注册这些语句。
  4. MapperBuilderAssistant 提供了设置命名空间、拼接完整的映射语句 ID 以及创建和注册 MappedStatement 对象的功能。
  5. MappedStatement 对象封装了 SQL 语句及其相关的元数据,并最终被注册到 Configuration 对象中,以便在运行时使用。

通过这一系列步骤,MyBatis 能够动态地管理 SQL 映射语句,并根据需要执行相应的数据库操作。


http://www.kler.cn/news/357071.html

相关文章:

  • 创始人抖音百科:品牌与形象的双重加速器!
  • vue综合指南(二)
  • 每日OJ题_牛客_最长回文子序列_区间DP_C++_Java
  • 腾讯云宝塔面板前后端项目发版
  • pyflink 时序异常检测——EWMA
  • 双链表(数据结构)——C语言
  • Git绑定Gitee或Github以及Git面试常见题
  • 100 种下划线 / 覆盖层动画 | 终极 CSS(层叠样式表)集合
  • MySQL的并行复制原理
  • 智能家居的“眼睛”:计算机视觉如何让家更智能
  • 【C++刷题】力扣-#88-合并两个有序数组
  • 光盘刻录大文件时分卷操作
  • C#中反射基础与应用
  • vueuse的常用方法记录
  • AI 视频工具合集
  • Python无监督学习中的聚类:K均值与层次聚类实现详解
  • 1.Node.js环境搭建(windows)
  • Python基础:20、Python基础综合案例
  • 如何使用python网络爬虫批量获取公共资源数据?
  • 六、存储过程和触发器及视图和临时表