MiniSpring框架学习-JDBC 访问框架: MiniBatis如何将 SQL 语句配置化?
- [15. MiniBatis:如何将 SQL 语句配置化?](#15. MiniBatis:如何将 SQL 语句配置化?)
-
- [一、先用 MyBatis 建立直觉](#一、先用 MyBatis 建立直觉)
- 二、先看最终使用方式
- [三、SQL 放到 Mapper XML](#三、SQL 放到 Mapper XML)
- [四、MapperNode:XML 中一条 SQL 的内存模型](#四、MapperNode:XML 中一条 SQL 的内存模型)
- [五、SqlSessionFactory:启动时解析 XML](#五、SqlSessionFactory:启动时解析 XML)
-
- [1. 配置入口](#1. 配置入口)
- [2. 初始化并扫描目录](#2. 初始化并扫描目录)
- [3. 解析 XML 并放入 Map](#3. 解析 XML 并放入 Map)
- [六、SqlSession:按 sqlId 执行 SQL](#六、SqlSession:按 sqlId 执行 SQL)
- 七、完整调用链
- 八、当前实现边界
教程: https://github.com/YaleGuo/minis
极客时间: 手把手带你写一个 MiniSpring
前言:这节源教程写的不错,清晰易懂,点赞!
15. MiniBatis:如何将 SQL 语句配置化?
前面几章里,JdbcTemplate 已经帮我们收起了 JDBC 的固定流程:获取连接、创建 PreparedStatement、绑定参数、关闭资源、转换异常。
但业务代码里还有一块东西很显眼:
java
final String sql = "select id, name, birthday from users where id = ?";
SQL 直接写在 Java 代码里可以工作,但当 SQL 变长、变复杂,或者需要多人协作维护时,Java 类会越来越乱。所以这一节模仿 MyBatis 的一个核心思路:把 SQL 放到 XML 里,Java 代码只通过一个 SQL id 去执行它。
一、先用 MyBatis 建立直觉
在真实 MyBatis 里,最基础的调用可以写成这样:
java
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = session.selectOne(
"org.mybatis.example.BlogMapper.selectBlog", 101);
}
这段代码表面上只有两步:
text
打开一次 SqlSession
↓
根据 statement id 执行一条 SQL
但它背后已经提前做过一件重要的事:MyBatis 会把配置文件和 Mapper XML 解析成内存里的元数据。比如下面这个 id:
text
org.mybatis.example.BlogMapper.selectBlog
通常可以拆成:
text
namespace = org.mybatis.example.BlogMapper
id = selectBlog
然后 MyBatis 就可以用 namespace + id 找到 XML 中配置好的 SQL、参数信息和结果映射规则。
粗略地说,MyBatis 的设计逻辑是:
text
启动阶段
读取配置和 Mapper XML
把每条 SQL 解析成内存对象
用 namespace.id 建立索引
运行阶段
业务代码传入 statement id 和参数
框架找到对应 SQL
绑定参数
执行 JDBC
把 ResultSet 映射成对象
这里有个小细节:openSession() 不是每次都重新加载 XML。XML 通常在 SqlSessionFactory 构建或初始化时已经解析好了;openSession() 更像是打开一次执行会话,让后续的 selectOne(...) 有地方执行 SQL、管理执行过程。
这么做的好处很直接:
- Java 代码更干净,业务方法不用塞一大段 SQL;
- SQL 有独立位置,长 SQL、复杂 SQL 更容易阅读和维护;
- SQL 可以用
namespace.id统一管理,不容易散落在各个 Service 里; - Java 层表达"我要做什么",XML 层表达"具体 SQL 怎么写"。
我们这一章不做完整 MyBatis,只先实现它里面最容易看清的一小段:SQL 配置化。
这一节实现的是教学版 MiniBatis,不是完整 MyBatis。它只完成这条主线:
text
Mapper XML
↓ 启动时解析
MapperNode
↓ 放入 Map,key = namespace.id
SqlSession.selectOne(sqlId, args, callback)
↓ 找到 SQL
JdbcTemplate.query(...)
二、先看最终使用方式
现在 UserService.getUserInfo(...) 不再直接写查询 SQL,而是写一个 SQL id:
java
package com.chenhai.jdbc.example;
import com.chenhai.beans.factory.annotation.Autowired;
import com.chenhai.batis.SqlSession;
import com.chenhai.batis.SqlSessionFactory;
import com.chenhai.jdbc.core.JdbcTemplate;
import java.sql.ResultSet;
import java.sql.Date;
import java.util.List;
/**
* 演示业务层怎样使用 JdbcTemplate 和教学版 SqlSession。
*
* 本节新增的变化是:查询 SQL 可以放到 Mapper XML 中,
* 业务代码只通过 SQL id 发起调用。
*/
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* SQL 配置化入口。字段名必须和 XML 中的 bean id="sqlSessionFactory" 一致。
*/
@Autowired
private SqlSessionFactory sqlSessionFactory;
public User getUserInfo(int userId) {
String sqlId = "com.chenhai.jdbc.example.User.getUserInfo";
SqlSession sqlSession = this.sqlSessionFactory.openSession();
return sqlSession.selectOne(sqlId, new Object[]{userId}, statement -> {
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? mapUser(resultSet) : null;
}
});
}
public List<User> getUsers(int minUserId) {
final String sql = "select id, name, birthday from users where id > ?";
return jdbcTemplate.query(sql, new Object[]{minUserId},
(resultSet, rowNum) -> mapUser(resultSet));
}
public int updateUserName(int userId, String name) {
final String sql = "update users set name = ? where id = ?";
return jdbcTemplate.update(sql, new Object[]{name, userId});
}
private User mapUser(ResultSet resultSet) throws java.sql.SQLException {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
Date birthday = resultSet.getDate("birthday");
if (birthday != null) {
user.setBirthday(new java.util.Date(birthday.getTime()));
}
return user;
}
}
对比一下变化:
text
上一章:
Java 代码直接写 SQL
这一章:
Java 代码写 sqlId
SQL 放在 mapper/UserMapper.xml
注意,当前教学版还没有做到"根据 Mapper 接口自动生成代理对象"。所以业务代码仍然手动调用:
java
sqlSession.selectOne(sqlId, args, callback)
结果映射也仍然由调用者传入的 PreparedStatementCallback 完成。
三、SQL 放到 Mapper XML
项目中的 XML 文件是:
text
src/main/resources/mapper/UserMapper.xml
内容如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.chenhai.jdbc.example.User">
<!--
namespace + id 组成 SQL 唯一标识:
com.chenhai.jdbc.example.User.getUserInfo
parameterType 和 resultType 当前主要用于教学说明,真正的参数绑定和结果映射
仍由 SqlSession 传入的 PreparedStatementCallback 完成。
-->
<select id="getUserInfo"
parameterType="java.lang.Integer"
resultType="com.chenhai.jdbc.example.User">
select id, name, birthday
from users
where id = ?
</select>
</mapper>
这里最关键的是两个属性:
| 属性 | 作用 |
|---|---|
namespace |
一组 SQL 的命名空间 |
id |
当前 SQL 在这个命名空间下的名字 |
两者拼起来就是 SQL id:
text
com.chenhai.jdbc.example.User.getUserInfo
这个 id 会作为 Map 的 key。运行时业务代码传入这个 key,就能找到对应 SQL。
parameterType 和 resultType 在当前版本里只是保存到 MapperNode 中,方便理解 MyBatis 的配置结构;真正的参数绑定还是 JdbcTemplate 做,结果映射还是回调做。
四、MapperNode:XML 中一条 SQL 的内存模型
XML 是文本,不适合每次查询时反复解析。更合理的方式是:容器启动时解析一次,把每条 SQL 变成 Java 对象放到内存里。
这个 Java 对象就是 MapperNode:
java
package com.chenhai.batis;
/**
* Mapper XML 中一条 SQL 语句的内存模型。
*
* MyBatis 不会在业务调用时才去读 XML,而是在启动阶段把 XML 解析成
* 类似 MapperNode 的对象并放入 Map。
*/
public class MapperNode {
private String namespace;
private String id;
private String parameterType;
private String resultType;
private String sql;
private String parameter;
public String getStatementId() {
return this.namespace + "." + this.id;
}
@Override
public String toString() {
return getStatementId() + " : " + this.sql;
}
// 省略普通 getter/setter
}
它对应 XML 里的这几块:
text
<mapper namespace="...">
<select id="..."
parameterType="..."
resultType="...">
SQL 文本
</select>
</mapper>
可以理解成:
text
Mapper XML MapperNode
---------- ----------
namespace ---> namespace
select.id ---> id
parameterType ---> parameterType
resultType ---> resultType
SQL 文本 ---> sql
五、SqlSessionFactory:启动时解析 XML
SqlSessionFactory 做两件事:
java
package com.chenhai.batis;
/**
* Factory 负责保存 Mapper 元数据并创建 SqlSession。
*/
public interface SqlSessionFactory {
SqlSession openSession();
MapperNode getMapperNode(String name);
}
默认实现是 DefaultSqlSessionFactory。它在容器启动时扫描 mapperLocations 指定的目录,解析里面的 XML。
1. 配置入口
applicationContext.xml 中这样配置:
xml
<!--
MiniBatis 教程配置。
1. mapperLocations 指向 classpath 下的 mapper 目录。
2. sqlSessionFactory 启动时解析 Mapper XML,把 namespace.id 映射到 SQL。
3. 业务代码只传 SQL id;真正执行仍然委托给 jdbcTemplate。
-->
<bean id="sqlSessionFactory"
class="com.chenhai.batis.DefaultSqlSessionFactory"
init-method="init">
<property type="String" name="mapperLocations" value="mapper"/>
</bean>
init-method="init" 表示 MiniSpring 创建完这个 Bean、注入完属性后,会调用 init()。
2. 初始化并扫描目录
java
public void init() {
if (this.mapperLocations == null || this.mapperLocations.trim().isEmpty()) {
throw new IllegalStateException("mapperLocations must be configured");
}
scanLocation(this.mapperLocations);
}
private void scanLocation(String location) {
URL locationUrl = getClass().getClassLoader().getResource(location);
if (locationUrl == null) {
throw new IllegalStateException("Mapper location not found: " + location);
}
File dir = new File(decodePath(locationUrl));
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File file : files) {
String childLocation = location + "/" + file.getName();
if (file.isDirectory()) {
scanLocation(childLocation);
} else if (file.getName().endsWith(".xml")) {
buildMapperNodes(childLocation);
}
}
}
这段逻辑和前面 IoC 扫描配置文件有点像:先找到 classpath 下的 mapper 目录,再递归处理里面的 XML 文件。
3. 解析 XML 并放入 Map
java
private Map<String, MapperNode> buildMapperNodes(String filePath) {
SAXReader saxReader = new SAXReader();
URL xmlPath = getClass().getClassLoader().getResource(filePath);
if (xmlPath == null) {
throw new IllegalStateException("Mapper XML not found: " + filePath);
}
try {
Document document = saxReader.read(xmlPath);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
if (namespace == null || namespace.trim().isEmpty()) {
throw new IllegalStateException(
"Mapper namespace must not be empty: " + filePath);
}
Iterator<?> nodes = rootElement.elementIterator();
while (nodes.hasNext()) {
Element node = (Element) nodes.next();
MapperNode mapperNode = buildMapperNode(namespace, node);
this.mapperNodeMap.put(mapperNode.getStatementId(), mapperNode);
}
return this.mapperNodeMap;
} catch (Exception e) {
throw new IllegalStateException("Parse mapper XML failed: " + filePath, e);
}
}
private MapperNode buildMapperNode(String namespace, Element node) {
String id = node.attributeValue("id");
if (id == null || id.trim().isEmpty()) {
throw new IllegalStateException(
"Mapper statement id must not be empty: " + namespace);
}
MapperNode mapperNode = new MapperNode();
mapperNode.setNamespace(namespace);
mapperNode.setId(id);
mapperNode.setParameterType(node.attributeValue("parameterType"));
mapperNode.setResultType(node.attributeValue("resultType"));
mapperNode.setSql(node.getTextTrim());
mapperNode.setParameter("");
return mapperNode;
}
解析完成后,内存里大概是这样:
text
mapperNodeMap
|
+-- key:
| com.chenhai.jdbc.example.User.getUserInfo
|
+-- value:
MapperNode {
namespace = "com.chenhai.jdbc.example.User"
id = "getUserInfo"
parameterType = "java.lang.Integer"
resultType = "com.chenhai.jdbc.example.User"
sql = "select id, name, birthday from users where id = ?"
}
业务调用时不用再读 XML,直接从这个 Map 里拿就行。
六、SqlSession:按 sqlId 执行 SQL
SqlSessionFactory 负责保存 SQL 配置,SqlSession 负责面向业务代码执行 SQL。
接口很小:
java
package com.chenhai.batis;
import com.chenhai.jdbc.core.JdbcTemplate;
import com.chenhai.jdbc.core.PreparedStatementCallback;
/**
* 当前教学版只实现 selectOne,并保留 PreparedStatementCallback 作为结果处理回调。
*/
public interface SqlSession {
void setJdbcTemplate(JdbcTemplate jdbcTemplate);
void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory);
<T> T selectOne(String sqlId,
Object[] args,
PreparedStatementCallback<T> callback);
}
创建 SqlSession 时,把两个依赖塞进去:
java
@Override
public SqlSession openSession() {
DefaultSqlSession sqlSession = new DefaultSqlSession();
sqlSession.setJdbcTemplate(this.jdbcTemplate);
sqlSession.setSqlSessionFactory(this);
return sqlSession;
}
这两个依赖分别负责:
| 依赖 | 作用 |
|---|---|
SqlSessionFactory |
根据 sqlId 找到 MapperNode |
JdbcTemplate |
执行 SQL、绑定参数、关闭资源 |
DefaultSqlSession.selectOne(...) 的核心代码是:
java
@Override
public <T> T selectOne(String sqlId,
Object[] args,
PreparedStatementCallback<T> callback) {
if (this.jdbcTemplate == null) {
throw new IllegalStateException("JdbcTemplate must be configured");
}
if (this.sqlSessionFactory == null) {
throw new IllegalStateException("SqlSessionFactory must be configured");
}
MapperNode mapperNode = this.sqlSessionFactory.getMapperNode(sqlId);
if (mapperNode == null) {
throw new IllegalArgumentException(
"No mapped SQL statement found: " + sqlId);
}
return this.jdbcTemplate.query(mapperNode.getSql(), args, callback);
}
所以 selectOne(...) 并没有重新实现一套 JDBC。它只是多做了一步:
text
sqlId
↓
MapperNode
↓
SQL 字符串
↓
JdbcTemplate.query(...)
七、完整调用链
再回到业务代码:
java
public User getUserInfo(int userId) {
String sqlId = "com.chenhai.jdbc.example.User.getUserInfo";
SqlSession sqlSession = this.sqlSessionFactory.openSession();
return sqlSession.selectOne(sqlId, new Object[]{userId}, statement -> {
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? mapUser(resultSet) : null;
}
});
}
实际执行过程是:
text
UserService.getUserInfo(1)
↓
sqlSessionFactory.openSession()
↓
创建 DefaultSqlSession
↓
注入 JdbcTemplate 和 SqlSessionFactory
↓
sqlSession.selectOne(sqlId, args, callback)
↓
sqlSessionFactory.getMapperNode(sqlId)
↓
取出 MapperNode.sql
↓
jdbcTemplate.query(sql, args, callback)
↓
ArgumentPreparedStatementSetter 绑定参数
↓
PreparedStatement.executeQuery()
↓
callback 把 ResultSet 转成 User
换成组件关系图就是:
text
UserService
|
| sqlId + args + callback
v
SqlSession
|
| 根据 sqlId 找 SQL
v
SqlSessionFactory
|
| mapperNodeMap
v
MapperNode
|
| sql
v
JdbcTemplate
|
| JDBC 固定流程
v
DataSource / Database
八、当前实现边界
这一章只实现了 SQL 配置化的第一步,重点是把"SQL 文本从 Java 代码移到 XML"讲清楚。
当前还没有实现:
- Mapper 接口代理;
#{id}这种命名参数解析;- 自动根据
resultType反射封装对象; <insert>、<update>、<delete>的独立语义;- 动态 SQL,例如
<if>、<where>、<foreach>; - 一级缓存、二级缓存;
- 事务和真实 MyBatis 的
SqlSession生命周期。
所以当前版本里:
text
parameterType / resultType
只是被解析并保存,还没有真正驱动参数绑定和结果映射。
PreparedStatementCallback
仍然由业务代码提供,用来执行 SQL 和处理 ResultSet。
JdbcTemplate
仍然是最终执行 JDBC 的核心。
对应测试主要验证两件事:
DefaultSqlSessionFactory能把mapper/UserMapper.xml解析成MapperNode;SqlSession.selectOne(...)能通过 SQL id 找到 SQL,并委托JdbcTemplate执行。
这一节可以总结成一句话:
MiniBatis 先不急着做完整 ORM,而是先把 SQL 配置化:启动时解析 XML,运行时按
namespace.id找 SQL,最后仍然交给JdbcTemplate执行。