手写MyBatis第41弹:MyBatis动态代理黑魔法:MapperProxy如何智能处理增删改的返回值?
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我。
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
目录
一、问题根源:JDBC执行结果与Java方法签名的鸿沟
二、解决方案:在MapperProxy.invoke中实现返回值适配
三、深入思考:这种灵活性的价值与延伸
四、总结与启示
-
MyBatis动态代理黑魔法:MapperProxy如何智能处理增删改的返回值?
-
手写MyBatis(五):MapperProxy的返回值适配策略,让接口设计更灵活
-
从void到boolean:深度解析MyBatis Mapper接口返回值类型的兼容性设计
-
不只是查询!MapperProxy如何处理增删改操作的多种返回值类型?
在前面的系列文章中,我们成功实现了MyBatis的Mapper动态代理,让一个简单的Java接口能够神奇地执行SQL查询。然而,细心的读者可能会发现,我们之前的实现主要聚焦于SELECT
查询操作。当我们转向INSERT
、UPDATE
、DELETE
等写操作时,一个新的挑战出现了:如何让Mapper接口中的增删改方法支持不同的返回值类型?
在MyBatis的实际使用中,我们经常会看到这样灵活的写法:
public interface UserMapper {void insertUser(User user); // 不关心返回值int updateUser(User user); // 想要知道影响了多少行boolean deleteUser(Long id); // 只关心是否成功Integer insertUserAndReturnCount(User user); // 包装类型也可以}
这种设计给予了开发者极大的便利。今天,我们就来深入剖析MapperProxy
如何通过巧妙的返回值类型适配策略,实现这种灵活性。
一、问题根源:JDBC执行结果与Java方法签名的鸿沟
要理解这个问题的本质,我们需要回到最基础的JDBC层面。无论是Statement
还是PreparedStatement
,执行增删改操作的方法都是executeUpdate()
,它的返回值是一个int
类型,表示受影响的行数(affected rows)。
然而,在Java接口设计中,开发者可能有着不同的意图:
-
不关心结果:只想执行操作,不需要知道结果,适合返回
void
。 -
想知道具体影响:需要确切知道修改了多少行数据,适合返回
int
。 -
只关心成败:只想知道操作是否成功(通常认为影响行数大于0就成功),适合返回
boolean
。
MapperProxy
的核心任务,就是架起一座桥梁,将JDBC返回的统一的int
类型,智能地转换适配成Mapper接口方法所声明的各种返回类型。
二、解决方案:在MapperProxy.invoke
中实现返回值适配
我们的改造主要在MapperProxy
的invoke
方法中进行。我们需要在执行SQL之后,根据方法返回类型的不同,对SqlSession
返回的原始int值进行二次加工。
步骤一:判断SQL命令类型
首先,我们需要确定当前调用的方法是增删改还是查询。这可以通过MappedStatement
中的信息获取(通常SQL命令类型保存在其中)。
public class MapperProxy implements InvocationHandler {// ... 其他字段private final SqlSession sqlSession;private final Configuration configuration;@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// ... 获取MappedStatement的逻辑MappedStatement ms = configuration.getMappedStatement(statementId);SqlCommandType sqlCommandType = ms.getSqlCommandType();// 根据SQL命令类型分支处理if (sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.UPDATE || sqlCommandType == SqlCommandType.DELETE) {// 处理增删改return handleUpdate(method, ms, args);} else {// 处理查询(原有逻辑)return handleQuery(method, ms, args);}}}
步骤二:执行并转换返回值(核心)
handleUpdate
方法是实现灵活性的关键。在这里,我们执行操作并完成类型转换。
private Object handleUpdate(Method method, MappedStatement ms, Object[] args) {// 1. 执行原始的增删改操作,获取受影响行数int affectedRows = sqlSession.update(ms.getId(), args[0]); // 简化处理,假设只有一个参数// 2. 获取Mapper接口方法的实际返回类型Class<?> returnType = method.getReturnType();// 3. 根据返回类型进行适配转换if (returnType == void.class || returnType == Void.class) {return null; // 返回void的方法,直接忽略返回值} else if (returnType == int.class || returnType == Integer.class) {return affectedRows; // 返回int或Integer,直接返回行数} else if (returnType == boolean.class || returnType == Boolean.class) {return affectedRows > 0; // 返回boolean,判断是否有影响} else if (returnType == long.class || returnType == Long.class) {return (long) affectedRows; // 支持Long类型} else {// 其他不支持的类型,可以抛出异常或进行其他处理throw new RuntimeException("Unsupported return type for update operation: " + returnType);}}
通过这段代码,我们可以看到MyBatis如何优雅地解决了返回值适配的问题。这种设计体现了框架设计的用户友好性原则:框架应该去适应开发者的习惯,而不是让开发者来适应框架的限制。
三、深入思考:这种灵活性的价值与延伸
1. 为什么MyBatis要提供这种灵活性?
这背后是深刻的API设计哲学:
-
意图导向:方法的返回值应该清晰表达开发者的意图。
void insert(...)
表示“执行插入,我不关心结果”;boolean delete(...)
表示“执行删除,告诉我成功与否”;int update(...)
表示“执行更新,告诉我具体影响了多少行”。这让代码更具可读性。 -
减少冗余代码:如果没有这种适配,开发者需要在业务代码中手动进行判断和转换,例如
int rows = sqlSession.update(...); return rows > 0;
。MapperProxy将这步操作内置,消除了模板代码。 -
保持接口简洁性:Mapper接口本身非常干净,不需要任何额外的注解来指定返回值处理方式,框架通过方法签名自动推断。
2. 如何获取自增主键?@Options注解的机制
返回值适配解决了“影响行数”的问题,但增删改操作还有一个常见需求:获取数据库自动生成的主键(如MySQL的AUTO_INCREMENT
)。
这在MyBatis中是通过@Options
注解或<insert>
标签的useGeneratedKeys
和keyProperty
属性实现的。它的原理是:
-
框架层面:MyBatis在执行
PreparedStatement
时,会通过Statement.RETURN_GENERATED_KEYS
参数告知JDBC驱动:“请返回生成的主键”。 -
JDBC驱动:驱动在执行插入后,会通过
statement.getGeneratedKeys()
方法返回一个包含生成主键的ResultSet。 -
结果处理:MyBatis拿到这个ResultSet后,会使用
ResultSetHandler
将值解析出来。 -
属性注入:最关键的一步,MyBatis通过反射,将解析出的主键值,设置到参数对象的
keyProperty
指定的属性中。
// 示例用法@Options(useGeneratedKeys = true, keyProperty = "id")int insertUser(User user);// 调用后,user对象的id属性会被自动赋值为数据库生成的主键userMapper.insertUser(user);System.out.println("Generated ID: " + user.getId()); // 这里可以取到值
值得注意的是,获取自增主键与Mapper方法的返回值是两个独立的过程。方法返回值仍然是受影响行数(适配后的值),而生成的主键被直接回填到了参数对象中。这种设计非常巧妙,同时满足了获取行数和获取主键两个需求。
四、总结与启示
通过对MapperProxy
返回值适配机制的深入剖析,我们看到了一个优秀框架在细节处的深思熟虑。它不仅仅是将JDBC操作简单封装,更是从开发者体验出发,提供了高度灵活和直观的API设计。
这种设计模式的精髓在于:在统一的底层实现(JDBC返回int)之上,构建一个能够理解用户意图并进行智能适配的中间层。这为我们设计自己的API和框架提供了宝贵的经验:
-
面向接口设计:从用户的使用场景和意图出发,设计最自然的接口。
-
提供适配层:在内部实现中,通过适配器模式消化底层差异,向用户提供一致的体验。
-
保持灵活性:通过反射等机制,实现运行时的动态决策,而不是在编译时写死逻辑。
现在,我们的手写MyBatis框架不仅能够执行CRUD,更能以多种形式向用户反馈结果,向着更加成熟和完善的方向迈进了一步。
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕我是程序员扣棣,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!