【从零开发Mybatis】引入MapperConfig.xml和Mapper映射配置

引言

学习MyBatis源码之前,了解它是如何通过JDBC查询数据库数据的基础知识是非常有用的。

上一篇我们编写了一个最简单的示例,通过JDBC查询数据库数据,从本文开始,我们将正式开始Mybatis框架的开发。

通过JDBC查询数据库数据存在的问题及处理方案

问题1:数据源写死在代码中

处理方案:引入MapperConfig.xml全局配置文件,配置数据源等信息

问题2:SQL语句写死在代码中

处理方案:引入Mapper映射文件,配置SQL脚本等信息

引入MapperConfig.xml

java 复制代码
<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    <dataSource >
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <mappers>
        <mapper resource="AuthorMapper.xml"/>
    </mappers>

</configuration>

以上XML 配置文件是一个典型的 MyBatis 配置文件的一部分,用于定义数据源(dataSource)和映射器(mapper)。这个配置文件定义了数据库连接的信息,并指定了一个映射器文件 AuthorMapper.xml。

引入Mapper映射文件

java 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">
    
    <select id="selectAuthor">
        select id, username, email from author where id = ?
    </select>

</mapper>
  • mapper 标签:定义一个 MyBatis 映射器,其中 namespace 属性指定了映射器的全限定类名。
  • namespace 属性:org.apache.ibatis.domain.blog.mappers.AuthorMapper,这是映射器的唯一标识符,用于区分不同的映射器。
  • select 标签:定义了一个 SQL 查询语句。
  • id 属性:selectAuthor,这是 SQL 语句的唯一标识符,用于在代码中引用此 SQL 语句。
  • SQL 语句:select id, username, email from author where id = ?,这是一个简单的 SQL 查询语句,用于从 author 表中选择特定记录。
  • ? 占位符:表示一个参数占位符,将在执行 SQL 语句时替换为实际值。

解析MapperConfig.xml和Mapper映射文件

我们对SqlSession类进行改造,数据源及SQL脚本通过读取MapperConfig.xml和Mapper映射文件获取,代码如下:

java 复制代码
package org.apache.ibatis.session;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.*;
import java.util.*;

/**
 * Sql会话
 *
 * @author crazy coder
 * @since 2024/9/27
 **/
public class SqlSession {
    public String selectOne(String statement, Integer param) throws ParserConfigurationException, XPathExpressionException, IOException, SAXException {

        final String configResource = "MapperConfig.xml";
        InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(configResource);
        Reader reader = new InputStreamReader(in);

        // 读取XML配置文件
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
        Document document = docBuilder.parse(new InputSource(reader));

        XPathFactory xPathFactory = XPathFactory.newInstance();
        XPath xpath = xPathFactory.newXPath();
        Node configNode = (Node) xpath.evaluate("/configuration", document, XPathConstants.NODE);

        // 解析XML配置信息 - 数据源
        // 驱动
        String driver = null;
        // 数据库连接 URL
        String url = null;
        // 数据库用户名
        String username = null;
        // 数据库密码
        String password = null;
        Node envNode = (Node) xpath.evaluate("dataSource", configNode, XPathConstants.NODE);
        NodeList nodeList = envNode.getChildNodes();
        for (int i = 0, n = nodeList.getLength(); i < n; i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Properties attributes = new Properties();
                NamedNodeMap attributeNodes = node.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        attributes.put(attribute.getNodeName(), value);
                    }
                }
                if ("driver".equals(attributes.get("name"))) {
                    driver = (String) attributes.get("value");
                } else if ("url".equals(attributes.get("name"))) {
                    url = (String) attributes.get("value");
                } else if ("username".equals(attributes.get("name"))) {
                    username = (String) attributes.get("value");
                } else if ("password".equals(attributes.get("name"))) {
                    password = (String) attributes.get("value");
                }
            }
        }

        // 读取Mapper配置文件
        List<String> resourceMapperList = new ArrayList<>();
        Node mappersNode = (Node) xpath.evaluate("mappers", configNode, XPathConstants.NODE);
        NodeList mapperList = mappersNode.getChildNodes();
        for (int i = 0, n = mapperList.getLength(); i < n; i++) {
            Node node = mapperList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                NamedNodeMap attributeNodes = node.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("resource".equals(attribute.getNodeName())) {
                            resourceMapperList.add(value);
                        }
                    }
                }
            }
        }
        Map<String, String> statementMap = new HashMap<>();
        for (String mapperResource : resourceMapperList) {
            try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) {
                Reader mapperReader = new InputStreamReader(inputStream);
                Document mapperDocument = docBuilder.parse(new InputSource(mapperReader));
                Node mapperNode = (Node) xpath.evaluate("/mapper", mapperDocument, XPathConstants.NODE);

                String namespace = "";
                NamedNodeMap mapperAttributeNodes = mapperNode.getAttributes();
                if (mapperAttributeNodes != null) {
                    for (int j = 0; j < mapperAttributeNodes.getLength(); j++) {
                        Node attribute = mapperAttributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("namespace".equals(attribute.getNodeName())) {
                            namespace = value;
                        }
                    }
                }

                Node selectNode = (Node) xpath.evaluate("select", mapperNode, XPathConstants.NODE);
                String mapperId = "";
                NamedNodeMap attributeNodes = selectNode.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("id".equals(attribute.getNodeName())) {
                            mapperId = value;
                        }
                    }
                }
                String sql = selectNode.getTextContent();
                statementMap.put(namespace + "." + mapperId, sql);
            }
        }


        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            // 加载 MySQL JDBC 驱动
            Class.forName(driver);

            // 获取数据库连接
            conn = DriverManager.getConnection(url, username, password);

            // 准备 SQL 语句
            String sql = statementMap.get(statement);

            // 创建预编译语句
            pstmt = conn.prepareStatement(sql);

            // 设置参数
            pstmt.setLong(1, param);

            // 执行 SQL 查询操作
            rs = pstmt.executeQuery();

            // 处理结果集
            StringBuilder result = new StringBuilder();
            while (rs.next()) {
                result.append("id: ").append(rs.getInt("id"))
                        .append(", username: ").append(rs.getString("username"))
                        .append(", email: ").append(rs.getString("email"));

            }
            return result.toString();

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            in.close();
        }

        return "";
    }
}

这段代码实现了通过 MyBatis 的配置文件读取数据源信息,并执行 SQL 查询的功能。下面是对这段代码的详细解析:

  • 方法定义 selectOne:
java 复制代码
public String selectOne(String statement, Integer param)
    throws ParserConfigurationException, XPathExpressionException, IOException, SAXException {

这个方法接受两个参数:statement 和 param。statement 是一个字符串,表示 SQL 语句的唯一标识符;param 是一个整数类型的参数,用于 SQL 语句中的占位符。

  • 读取MapperConfig.xml配置文件
java 复制代码
final String configResource = "MapperConfig.xml";
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(configResource);
Reader reader = new InputStreamReader(in);

// 读取XML配置文件
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document document = docBuilder.parse(new InputSource(reader));

这里使用了 DOM 解析器来读取配置文件 MapperConfig.xml,并将文件内容解析成 Document 对象。

  • 解析数据源信息
java 复制代码
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
Node configNode = (Node) xpath.evaluate("/configuration", document, XPathConstants.NODE);

// 解析XML配置信息 - 数据源
// 驱动
String driver = null;
// 数据库连接 URL
String url = null;
// 数据库用户名
String username = null;
// 数据库密码
String password = null;
Node envNode = (Node) xpath.evaluate("dataSource", configNode, XPathConstants.NODE);
NodeList nodeList = envNode.getChildNodes();
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
    Node node = nodeList.item(i);
    if (node.getNodeType() == Node.ELEMENT_NODE) {
        Properties attributes = new Properties();
        NamedNodeMap attributeNodes = node.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                attributes.put(attribute.getNodeName(), value);
            }
        }
        if ("driver".equals(attributes.get("name"))) {
            driver = (String) attributes.get("value");
        } else if ("url".equals(attributes.get("name"))) {
            url = (String) attributes.get("value");
        } else if ("username".equals(attributes.get("name"))) {
            username = (String) attributes.get("value");
        } else if ("password".equals(attributes.get("name"))) {
            password = (String) attributes.get("value");
        }
    }
}

这部分代码解析了配置文件中的 节点,并从中提取出数据库连接所需的各项信息:驱动、URL、用户名和密码。

  • 读取 Mapper 文件
java 复制代码
List<String> resourceMapperList = new ArrayList<>();
Node mappersNode = (Node) xpath.evaluate("mappers", configNode, XPathConstants.NODE);
NodeList mapperList = mappersNode.getChildNodes();
for (int i = 0, n = mapperList.getLength(); i < n; i++) {
    Node node = mapperList.item(i);
    if (node.getNodeType() == Node.ELEMENT_NODE) {
        NamedNodeMap attributeNodes = node.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("resource".equals(attribute.getNodeName())) {
                    resourceMapperList.add(value);
                }
            }
        }
    }
}

这部分代码解析了配置文件中的 节点,并从中提取出映射器文件的路径列表。

  • 解析 SQL 语句
java 复制代码
Map<String, String> statementMap = new HashMap<>();
for (String mapperResource : resourceMapperList) {
    try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) {
        Reader mapperReader = new InputStreamReader(inputStream);
        Document mapperDocument = docBuilder.parse(new InputSource(mapperReader));
        Node mapperNode = (Node) xpath.evaluate("/mapper", mapperDocument, XPathConstants.NODE);

        String namespace = "";
        NamedNodeMap mapperAttributeNodes = mapperNode.getAttributes();
        if (mapperAttributeNodes != null) {
            for (int j = 0; j < mapperAttributeNodes.getLength(); j++) {
                Node attribute = mapperAttributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("namespace".equals(attribute.getNodeName())) {
                    namespace = value;
                }
            }
        }

        Node selectNode = (Node) xpath.evaluate("select", mapperNode, XPathConstants.NODE);
        String mapperId = "";
        NamedNodeMap attributeNodes = selectNode.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("id".equals(attribute.getNodeName())) {
                    mapperId = value;
                }
            }
        }
        String sql = selectNode.getTextContent();
        statementMap.put(namespace + "." + mapperId, sql);
    }
}

这部分代码解析了每个映射器文件中的 节点,并从中提取出 SQL 语句的 namespace 和 id,并将它们组合成键值对存储在 statementMap 中。

  • 执行 SQL 查询
java 复制代码
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

try {
    // 加载 MySQL JDBC 驱动
    Class.forName(driver);

    // 获取数据库连接
    conn = DriverManager.getConnection(url, username, password);

    // 准备 SQL 语句
    String sql = statementMap.get(statement);

    // 创建预编译语句
    pstmt = conn.prepareStatement(sql);

    // 设置参数
    pstmt.setLong(1, param);

    // 执行 SQL 查询操作
    rs = pstmt.executeQuery();

    // 处理结果集
    StringBuilder result = new StringBuilder();
    while (rs.next()) {
        result.append("id: ").append(rs.getInt("id"))
              .append(", username: ").append(rs.getString("username"))
              .append(", email: ").append(rs.getString("email"));
    }
    return result.toString();

} catch (ClassNotFoundException | SQLException e) {
    e.printStackTrace();
} finally {
    // 关闭资源
    if (pstmt != null) {
        try {
            pstmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    in.close();
}

return "";

这部分代码实现了通过 JDBC 连接到数据库,并执行 SQL 查询的操作。它使用预编译语句来提高安全性,并正确处理了结果集。

测试用例

java 复制代码
package org.apache.ibatis.session;

import org.junit.jupiter.api.Test;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import java.io.IOException;

class SqlSessionTest {

    @Test
    void selectOne() throws ParserConfigurationException, SAXException, XPathExpressionException, IOException {
        SqlSession sqlSession = new SqlSession();
        String statement = "org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor";
        System.out.println(sqlSession.selectOne(statement, 101));
    }
}

测试类 SqlSessionTest 包含了一个 JUnit 测试用例 selectOne,用于测试 SqlSession 类中的 selectOne 方法。

整体项目结构

总结

本文我们实现了以下功能:

  • 加载配置文件:从类路径中加载 MapperConfig.xml 文件。
  • 解析数据源信息:提取MapperConfig.xml 文件中的数据库连接信息(驱动、URL、用户名和密码)。
  • 读取 Mapper 文件:读取MapperConfig.xml 文件中指定的 Mapper 文件。解析 Mapper 文件中的 SQL 语句,并将 SQL 语句及其标识符存储在 Map 中。
相关推荐
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
GoodStudyAndDayDayUp5 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
见欢.6 小时前
XXE靶场
xml
cmdch201711 小时前
Mybatis加密解密查询操作(sql前),where要传入加密后的字段时遇到的问题
数据库·sql·mybatis
秋恬意13 小时前
什么是MyBatis
mybatis
CodeChampion13 小时前
60.基于SSM的个人网站的设计与实现(项目 + 论文)
java·vue.js·mysql·spring·elementui·node.js·mybatis
云和数据.ChenGuang19 小时前
《XML》教案 第1章 学习XML基础
xml·java·学习
王·小白攻城狮·不是那么帅的哥·天文19 小时前
Java操作Xml
xml·java
xiao_fwuu1 天前
IDEA 打开 maven 的 settings.xml 文件
xml·maven·intellij-idea
ZWZhangYu1 天前
【MyBatis源码分析】使用 Java 动态代理,实现一个简单的插件机制
java·python·mybatis