🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我。
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
目录
-
MyBatis增删改查全解析:从注解到执行的完整链路实现
-
手写MyBatis的CRUD扩展:支持@Insert、@Update、@Delete注解的原理与实现
-
深入MyBatis执行引擎:增删改操作如何影响数据库并返回结果
-
数据库操作进阶:MyBatis如何优雅处理主键回写和受影响行数
-
从注解到SQL执行:揭秘MyBatis增删改操作的完整生命周期
正文
在前几篇文章中,我们实现了MyBatis的查询功能,搭建了基本的框架结构。然而,一个完整的ORM框架必须支持完整的CRUD(增删改查)操作。今天,我们将深入探讨MyBatis如何支持@Insert
、@Update
、@Delete
注解以及XML中的增删改标签,并解析其中的关键技术细节。
一、增删改操作的特殊性:与查询的本质区别
增删改操作与查询操作在多个层面存在根本性差异:
-
返回值类型:查询操作返回结果集对象,而增删改操作返回受影响的行数(int类型)
-
SQL命令类型:需要明确区分SELECT、INSERT、UPDATE、DELETE四种操作类型
-
主键处理:插入操作通常需要处理自增主键的回写问题
-
事务敏感性:增删改操作对事务的要求更加严格
这些差异要求我们在框架设计中做出相应的扩展和调整。
二、架构扩展:支持多种SQL命令类型
首先,我们需要在架构层面支持不同的SQL命令类型。以下是增删改操作在MyBatis框架中的完整执行流程:
MapperProxySqlSessionExecutorStatementHandlerDatabase调用update/insert/delete方法执行update操作准备语句参数绑定执行SQL返回受影响行数返回结果处理主键生成(可选)返回受影响行数返回结果MapperProxySqlSessionExecutorStatementHandlerDatabase
第一步:定义SQL命令类型枚举
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT;
public static SqlCommandType valueOf(Class<? extends Annotation> annotationType) {
if (annotationType == Select.class) {
return SELECT;
} else if (annotationType == Insert.class) {
return INSERT;
} else if (annotationType == Update.class) {
return UPDATE;
} else if (annotationType == Delete.class) {
return DELETE;
}
return UNKNOWN;
}
}
第二步:扩展MappedStatement记录命令类型
public class MappedStatement {
private String id;
private SqlCommandType sqlCommandType;
private String sql;
private Class<?> parameterType;
private Class<?> resultType;
private boolean useGeneratedKeys = false;
private String keyProperty;
// 构造方法、getter、setter
}
三、MapperProxy的增强:识别并路由增删改操作
在MapperProxy
中,我们需要根据方法注解类型来决定调用SqlSession
的哪个方法:
public class MapperProxy<T> implements InvocationHandler {
// ... 其他代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 处理Object类的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
String statementId = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(statementId);
// 根据SQL命令类型路由到不同的SqlSession方法
switch (ms.getSqlCommandType()) {
case SELECT:
// 处理查询...
break;
case INSERT:
case UPDATE:
case DELETE:
return executeUpdate(sqlSession, ms, args);
default:
throw new RuntimeException("Unknown SQL command type: " + ms.getSqlCommandType());
}
}
private Object executeUpdate(SqlSession sqlSession, MappedStatement ms, Object[] args) {
Object parameter = getParameter(args);
int affectedRows = sqlSession.update(ms.getId(), parameter);
// 处理返回值类型
Class<?> returnType = method.getReturnType();
if (returnType == void.class || returnType == Void.class) {
return null;
} else if (returnType == int.class || returnType == Integer.class) {
return affectedRows;
} else if (returnType == boolean.class || returnType == Boolean.class) {
return affectedRows > 0;
} else {
throw new RuntimeException("Unsupported return type for update operation: " + returnType);
}
}
}
四、Executor的update方法实现:处理增删改操作
Executor
的update
方法需要处理SQL执行并返回受影响的行数:
public class SimpleExecutor extends BaseExecutor {
@Override
public int update(MappedStatement ms, Object parameter) {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter);
stmt = prepareStatement(handler);
int rows = handler.update(stmt);
// 处理主键生成
if (ms.isUseGeneratedKeys() && ms.getKeyProperty() != null) {
handleGeneratedKeys(ms, parameter, stmt);
}
return rows;
} finally {
closeStatement(stmt);
}
}
private void handleGeneratedKeys(MappedStatement ms, Object parameter, Statement stmt) {
try {
ResultSet generatedKeys = stmt.getGeneratedKeys();
if (generatedKeys.next()) {
Object key = generatedKeys.getObject(1);
// 使用反射将生成的主键值设置到参数对象中
String keyProperty = ms.getKeyProperty();
setValueToObject(parameter, keyProperty, key);
}
} catch (SQLException e) {
throw new RuntimeException("Error getting generated keys", e);
}
}
private void setValueToObject(Object obj, String property, Object value) {
// 使用反射设置属性值
try {
Field field = obj.getClass().getDeclaredField(property);
field.setAccessible(true);
field.set(obj, value);
} catch (Exception e) {
throw new RuntimeException("Error setting generated key to property: " + property, e);
}
}
}
五、主键回写:MyBatis的巧妙设计
主键回写是插入操作中的一个重要特性,MyBatis提供了多种方式来处理:
1. XML配置方式:
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>
2. 注解配置方式:
@Insert("INSERT INTO user(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
3. SelectKey方式(适用于非自增主键):
<insert id="insertUser" parameterType="User">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT nextval('user_id_seq')
</selectKey>
INSERT INTO user(id, name, email) VALUES(#{id}, #{name}, #{email})
</insert>
六、事务处理与性能考量
增删改操作对事务有更高的要求,我们需要确保:
-
事务一致性:在同一个SqlSession中的多个操作应该共享同一个事务
-
自动提交控制:根据配置决定是否自动提交事务
-
批量操作优化:支持批处理执行器提高批量操作的性能
public class BatchExecutor extends SimpleExecutor {
private List<Statement> statementList = new ArrayList<>();
private List<BatchResult> batchResults = new ArrayList<>();
@Override
public int update(MappedStatement ms, Object parameter) {
// 批量处理逻辑
// ...
}
@Override
public List<BatchResult> flushStatements() {
// 执行批量操作
// ...
}
}
七、总结与最佳实践
通过本次扩展,我们的MyBatis框架已经支持了完整的CRUD操作。在这个过程中,我们解决了几个关键问题:
-
命令类型识别:通过SqlCommandType区分不同操作
-
返回值处理:正确处理受影响行数和各种返回类型
-
主键回写:支持自动主键回写到实体对象
-
事务管理:确保增删改操作的事务一致性
最佳实践建议:
-
对于插入操作,尽量使用
useGeneratedKeys
来简化主键处理 -
批量操作时使用
BatchExecutor
显著提升性能 -
明确方法的返回值类型(void、int、boolean),保持一致性
-
在事务边界清晰的地方手动控制事务提交
增删改操作的实现不仅完善了框架功能,更让我们深入理解了MyBatis的设计哲学:通过简洁的API隐藏复杂的实现细节。这种设计使得开发者可以专注于业务逻辑,而无需关心底层的数据库操作细节。
下次当你使用@Insert
注解时,不妨想一想背后的复杂流程:从注解解析到SQL生成,从参数绑定到主键回写,这一切都在MyBatis的精心设计下变得如此简单而优雅。
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕我是程序员扣棣,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!