引言
学习MyBatis源码之前,了解它是如何通过JDBC查询数据库数据的基础知识是非常有用的。
上一篇我们编写了一个最简单的示例,通过JDBC查询数据库数据,从本文开始,我们将正式开始Mybatis框架的开发。
通过JDBC查询数据库数据存在的问题及处理方案
问题1:数据源写死在代码中
处理方案:引入MapperConfig.xml全局配置文件,配置数据源等信息
问题2:SQL语句写死在代码中
处理方案:引入Mapper映射文件,配置SQL脚本等信息
引入MapperConfig.xml
<?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&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映射文件
<?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映射文件获取,代码如下:
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:
public String selectOne(String statement, Integer param)
throws ParserConfigurationException, XPathExpressionException, IOException, SAXException {
这个方法接受两个参数:statement 和 param。statement 是一个字符串,表示 SQL 语句的唯一标识符;param 是一个整数类型的参数,用于 SQL 语句中的占位符。
- 读取MapperConfig.xml配置文件
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 对象。
- 解析数据源信息
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 文件
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 语句
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 查询
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 查询的操作。它使用预编译语句来提高安全性,并正确处理了结果集。
测试用例
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 中。