手写MyBatis第28弹:告别代理,直击本质:手写MyBatis SqlSession的增删改查奥秘 - 指南

(❁´◡`❁)您的点赞➕评论➕收藏⭐是作者创作的最大动力

支持我:点赞+收藏⭐️+留言欢迎留言讨论

(源码 + 调试运行 + 问题答疑)

 有兴趣可以联系我。

我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。

目录

热门标题推荐

正文

一、为何需要非Mapper代理的API?

二、核心架构与执行流程剖析

三、手写实现:从接口定义到具体执行

四、实战演示:如何使用底层API五、总结与思考


  1. 深入MyBatis内核:徒手打造SqlSession的CRUD圣殿

  2. 告别代理,直击本质:手写MyBatis SqlSession的增删改查奥秘

  3. MyBatis源码进阶(一):从SqlSession的CRUD接口看框架执行脉络

  4. “硬核”操作:在手写MyBatis中实现最底层的SQL执行API

  5. 架构师视角:为什么SqlSession的直接CRUD API是MyBatis灵活性的基石?


正文

在我们日常使用MyBatis进行开发时,最熟悉的莫过于定义Mapper接口并配合@Select@Insert等注解或XML文件来轻松实现数据库操作。这背后是MyBatis强大的动态代理机制在默默工作。但你是否曾想过,抛开这层“魔法”,MyBatis最核心、最底层的执行单元究竟是什么?今天,我们就将深入MyBatis的内核,亲手实现SqlSession接口中的直接CRUD方法,探究其原理、价值与设计哲学。

一、为何需要非Mapper代理的API?

在回答如何实现之前,我们必须先理解为什么要提供这样一种看似“繁琐”的API。Mapper代理的方式固然优雅,但它并非万能。

  1. 极致灵活性与动态性:在某些高度动态的场景中,SQL语句的statementId(即namespace.id)甚至SQL片段本身都可能是在运行时根据业务逻辑生成的。此时,我们无法在编码阶段定义出固定的Mapper接口方法。通过SqlSession直接传入statementId的方式,可以灵活地执行任何在配置中定义的SQL映射。

  2. 理解框架执行脉络:Mapper代理只是一个“外观模式”(Facade Pattern)的优雅实现,它最终调用的仍然是SqlSessionselectOneinsert等方法。学习直接使用这些方法,有助于我们更深层次地理解MyBatis的执行流程和数据流转,这对于排查复杂问题、进行性能调优至关重要。

  3. 遗留系统或特殊集成:在一些古老的系统或者特殊的集成需求中,可能无法按照MyBatis的约定来定义接口。这种底层API提供了更大的集成空间。

  4. 框架扩展的基石:许多基于MyBatis的增强框架或中间件(如分库分表组件),其内部往往需要绕过Mapper代理,直接与最核心的ExecutorSqlSession打交道。理解这一层,是进行二次开发的基础。

正因为其底层和灵活的特性,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是接口的默认实现,它持有ConfigurationExecutor的引用,所有方法的核心逻辑都是:

  1. 通过Configuration获取对应的MappedStatement

  2. 将参数和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的所有数据库操作命令都委托给它来完成。它定义了queryupdatecommitrollback等方法。我们的默认实现(SimpleExecutor)会处理整个执行流程:

    1. 创建StatementHandler(它负责创建PreparedStatement)。

    2. 创建ParameterHandler(它负责将用户传入的参数按规则设置到SQL中)。

    3. 执行语句。

    4. 创建ResultSetHandler(它负责将返回的ResultSet转换成Java对象集合)。

  • 一致性:注意到insertupdatedelete最终都调用了executor.update()。这说明在JDBC层面和MyBatis底层视角中,只有SELECTUPDATE两种操作INSERTDELETE在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时更加得心应手,充满自信。


学习知识需费心,
整理归纳更费神。
源码免费人人喜,
码农福利等你领!

常来我家多看看,
我是程序员扣棣,
感谢支持常陪伴,
点赞关注别忘记!

山高路远坑又深,
大军纵横任驰奔,
谁敢横刀立马行?
唯有点赞+关注成!

posted @ 2025-08-24 11:24  yjbjingcha  阅读(8)  评论(0)    收藏  举报