【从零开发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 中。
相关推荐
开往198230 分钟前
spring boot整合mybatis
java·spring boot·mybatis
guojl1 小时前
MyBatis应用案例
后端·mybatis
中东大鹅1 小时前
Mybatis Plus 多数据源
java·数据库·spring boot·后端·mybatis
苦学编程的谢2 小时前
Mybatis_2
java·开发语言·后端·java-ee·mybatis
新world13 小时前
mybatis-plus从入门到入土(三):持久层接口之IService
mybatis
阿华的代码王国18 小时前
【Android】相对布局应用-登录界面
android·xml·java
工藤新一OL19 小时前
把xml的格式从utf-8-bom转为utf-8
xml·c#·asp.net·.netcore·visual studio
苦学编程的谢21 小时前
MyBatis_3
java·开发语言·后端·mybatis
guojl21 小时前
MyBatis最佳实践
后端·微服务·mybatis
henysugar1 天前
便捷删除Android开发中XML中重复字符串资源的一个办法
android·xml