手写MyBatis第28弹:告别代理,直击本质:手写MyBatis SqlSession的增删改查奥秘 - 指南
(❁´◡`❁)您的点赞➕评论➕收藏⭐是作者创作的最大动力
支持我:点赞+收藏⭐️+留言欢迎留言讨论
(源码 + 调试运行 + 问题答疑)
有兴趣可以联系我。
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
目录
深入MyBatis内核:徒手打造SqlSession的CRUD圣殿
告别代理,直击本质:手写MyBatis SqlSession的增删改查奥秘
MyBatis源码进阶(一):从SqlSession的CRUD接口看框架执行脉络
“硬核”操作:在手写MyBatis中实现最底层的SQL执行API
架构师视角:为什么SqlSession的直接CRUD API是MyBatis灵活性的基石?
正文
在我们日常使用MyBatis进行开发时,最熟悉的莫过于定义Mapper接口并配合@Select、@Insert等注解或XML文件来轻松实现数据库操作。这背后是MyBatis强大的动态代理机制在默默工作。但你是否曾想过,抛开这层“魔法”,MyBatis最核心、最底层的执行单元究竟是什么?今天,我们就将深入MyBatis的内核,亲手实现SqlSession接口中的直接CRUD方法,探究其原理、价值与设计哲学。
一、为何需要非Mapper代理的API?
在回答如何实现之前,我们必须先理解为什么要提供这样一种看似“繁琐”的API。Mapper代理的方式固然优雅,但它并非万能。
极致灵活性与动态性:在某些高度动态的场景中,SQL语句的
statementId(即namespace.id)甚至SQL片段本身都可能是在运行时根据业务逻辑生成的。此时,我们无法在编码阶段定义出固定的Mapper接口方法。通过SqlSession直接传入statementId的方式,可以灵活地执行任何在配置中定义的SQL映射。理解框架执行脉络:Mapper代理只是一个“外观模式”(Facade Pattern)的优雅实现,它最终调用的仍然是
SqlSession的selectOne、insert等方法。学习直接使用这些方法,有助于我们更深层次地理解MyBatis的执行流程和数据流转,这对于排查复杂问题、进行性能调优至关重要。遗留系统或特殊集成:在一些古老的系统或者特殊的集成需求中,可能无法按照MyBatis的约定来定义接口。这种底层API提供了更大的集成空间。
框架扩展的基石:许多基于MyBatis的增强框架或中间件(如分库分表组件),其内部往往需要绕过Mapper代理,直接与最核心的
Executor和SqlSession打交道。理解这一层,是进行二次开发的基础。
正因为其底层和灵活的特性,SqlSession实例的生命周期管理就显得尤为重要。它通常不是线程安全的,因此其生命周期应被限定在一次请求或一个方法作用域内(即“请求级别”)。通过SqlSessionFactory来创建,在使用后务必通过close()方法将其关闭,以防止数据库连接泄漏。这也是为什么我们常用try-with-resources语句来确保其被正确关闭。
二、核心架构与执行流程剖析
在开始手写代码之前,我们先通过一张图来俯瞰整个执行流程,这有助于理解各个组件是如何协同工作的:

如图所示,SqlSession是API的入口,但它只是一个“指挥中心”,真正的执行工作委托给了Executor。而Executor又会进一步将参数处理、SQL执行、结果集映射等任务分发给各自的专职组件(StatementHandler, ParameterHandler, ResultSetHandler)。整个流程清晰且职责分明,是经典职责链模式的体现。
我们的核心任务,就是在DefaultSqlSession中实现这个“指挥”的逻辑。
三、手写实现:从接口定义到具体执行
现在,让我们开始动手,一步步实现图中的流程。
第一步:定义SqlSession接口的CRUD方法
我们在SqlSession接口中直接定义增删改查方法。
public interface SqlSession extends Closeable {
/**
* 检索一条记录
* @param statementId namespace.id
* @param parameter 参数对象
* @return 单条结果对象
*/
T selectOne(String statementId, Object parameter);
/**
* 插入一条记录
* @param statementId namespace.id
* @param parameter 参数对象
* @return 影响的行数
*/
int insert(String statementId, Object parameter);
// 类似定义 update, delete 方法
int update(String statementId, Object parameter);
int delete(String statementId, Object parameter);
// ... 其他方法如 getMapper, getConfiguration, commit, rollback 等
}
第二步:在DefaultSqlSession中实现这些方法
DefaultSqlSession是接口的默认实现,它持有Configuration和Executor的引用,所有方法的核心逻辑都是:
通过
Configuration获取对应的MappedStatement。将参数和
MappedStatement交给Executor去执行。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public T selectOne(String statementId, Object parameter) {
// 本质上就是调用selectList,然后取第一条结果
List list = this.selectList(statementId, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new RuntimeException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
@Override
public List selectList(String statementId, Object parameter) {
// 1. 核心:根据statementId从配置中获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statementId);
// 2. 委托给Executor执行
return executor.query(ms, parameter);
}
@Override
public int insert(String statementId, Object parameter) {
// 增删改本质上都是update操作
return update(statementId, parameter);
}
@Override
public int update(String statementId, Object parameter) {
MappedStatement ms = configuration.getMappedStatement(statementId);
return executor.update(ms, parameter);
}
@Override
public int delete(String statementId, Object parameter) {
return update(statementId, parameter);
}
// ... 关闭、提交、回滚等方法
}
关键点剖析:
MappedStatement:它是MyBatis的核心数据结构之一,是一个不可变的对象。它完整地描述了一条SQL语句的所有信息:SQL源、执行类型(SELECT/UPDATE等)、输入参数映射、输出结果映射、缓存策略、ID等。通过statementId(格式为namespace.id)从Configuration这个全局配置容器中获取它,是执行的第一步。Executor:它是真正的执行器,是MyBatis的调度核心。SqlSession的所有数据库操作命令都委托给它来完成。它定义了query、update、commit、rollback等方法。我们的默认实现(SimpleExecutor)会处理整个执行流程:创建
StatementHandler(它负责创建PreparedStatement)。创建
ParameterHandler(它负责将用户传入的参数按规则设置到SQL中)。执行语句。
创建
ResultSetHandler(它负责将返回的ResultSet转换成Java对象集合)。
一致性:注意到
insert、update、delete最终都调用了executor.update()。这说明在JDBC层面和MyBatis底层视角中,只有SELECT和UPDATE两种操作(INSERT和DELETE在JDBC中也属于更新操作)。这种设计简化了内部实现。
四、实战演示:如何使用底层API
假设我们在XML中配置了一个SQL:
SELECT * FROM employee WHERE id = #{id}
使用Mapper代理方式,我们会这样调用:
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee emp = mapper.selectById(1);
而使用我们今天实现的底层API,则可以这样调用:
// 注意:statementId 必须是 "namespace.id" 的全限定名
String statementId = "com.example.EmployeeMapper.selectById";
Employee emp = sqlSession.selectOne(statementId, 1);
可以看到,这种方式确实更为繁琐,需要手动拼接字符串且容易出错,但它赋予了我们在运行时动态选择SQL的巨大灵活性。
五、总结与思考
通过手写SqlSession的CRUD方法,我们剥离了MyBatis的动态代理“外衣”,直击其核心执行引擎。我们看到了SqlSession如何作为API门面,Configuration如何作为配置中枢,Executor如何作为调度核心,以及MappedStatement如何作为SQL指令的载体。
这种底层API的存在,不仅是MyBatis框架灵活性和扩展性的体现,更是我们开发者深入理解框架、解决复杂问题的钥匙。它告诉我们,任何一个优秀的框架,其高层抽象的便捷性必然构建在底层坚实的灵活性和可扩展性之上。
下次当你轻松地调用一个Mapper接口方法时,不妨在脑海中回顾一下这条清晰的执行链:Interface -> Proxy -> SqlSession -> Executor -> JDBC Statement。这份理解,将让你在使用MyBatis时更加得心应手,充满自信。
学习知识需费心,
整理归纳更费神。
源码免费人人喜,
码农福利等你领!常来我家多看看,
我是程序员扣棣,
感谢支持常陪伴,
点赞关注别忘记!山高路远坑又深,
大军纵横任驰奔,
谁敢横刀立马行?
唯有点赞+关注成!
浙公网安备 33010602011771号