diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/SelectForUpdateMethod.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/SelectForUpdateMethod.java
index 454b758..de4f7d9 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/SelectForUpdateMethod.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/SelectForUpdateMethod.java
@@ -47,14 +47,17 @@ public class SelectForUpdateMethod extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class> mapperClass, Class> modelClass, TableInfo tableInfo) {
- // 没有主键的表,无法查询首行主键
+ // 1. 没有主键的表,无法查询首行主键
if (kind == Kind.FIRST_IDS && !tableInfo.havePK()) {
return null;
}
+ // 2. 拼接 SQL:查询列 + 条件 + 排序与锁定;首行主键查询只查询主键列
String columns = kind == Kind.FIRST_IDS ? tableInfo.getKeyColumn() : sqlSelectColumns(tableInfo, true);
String sql = "";
+ // 3. 使用 LockSqlSource 标记为锁定读的内置查询,供 ForUpdateInnerInterceptor 识别
SqlSource sqlSource = new LockSqlSource(kind, super.createSqlSource(configuration, sql, modelClass));
+ // 4. 注册 MappedStatement:首行主键查询的结果为主键类型,其它为实体(沿用表的 resultMap)
if (kind == Kind.FIRST_IDS) {
return addSelectMappedStatementForOther(mapperClass, methodName, sqlSource, tableInfo.getKeyType());
}
@@ -68,16 +71,21 @@ public class SelectForUpdateMethod extends AbstractMethod {
* @return WHERE 脚本
*/
private String sqlWhere(TableInfo tableInfo) {
+ // 实体条件:Wrapper 设置了 entity 时生效
String entityWhere = SqlScriptUtils.convertIf(tableInfo.getAllSqlWhere(true, true, true, WRAPPER_ENTITY_DOT),
WRAPPER_ENTITY + " != null", true);
+ // 普通条件:整体加括号,避免 OR 条件绕过后面的主键、逻辑删除条件
String normalWhere = SqlScriptUtils.convertIf(" AND (${" + WRAPPER_DOT + "expression.normal.sqlSegment})",
WRAPPER_DOT + "nonEmptyOfNormal", true);
+ // 主键:仅加锁查询、且表有主键时,按传入的主键限制,用于 Oracle 的首行锁定
String idWhere = kind == Kind.LOCK && tableInfo.havePK() ? NEWLINE + SqlScriptUtils.convertIf(
" AND " + tableInfo.getKeyColumn() + " = " + SqlScriptUtils.safeParam(PARAM_ID), PARAM_ID + " != null", true) : "";
+ // 拼接逻辑删除条件,由 去除开头多余的 AND
return SqlScriptUtils.convertWhere(entityWhere + NEWLINE + normalWhere + idWhere
+ NEWLINE + tableInfo.getLogicDeleteSql(true, true));
}
+ // DONE @AI:方法内 注释;(处理:已按主键查询、加锁查询两种情况补充注释)
/**
* 排序与锁定:
* 1. 加锁查询:Wrapper 的排序(未设置时不排序,避免改变数据库的加锁范围) + FOR UPDATE;首行再加 LIMIT 1
@@ -88,19 +96,23 @@ public class SelectForUpdateMethod extends AbstractMethod {
*/
private String sqlTail(TableInfo tableInfo) {
String orderBy = " ${" + WRAPPER_DOT + "expression.orderBy.sqlSegment}";
+ // 情况一:首行主键查询,不加锁;未设置排序时按主键排序
if (kind == Kind.FIRST_IDS) {
return SqlScriptUtils.convertChoose(WRAPPER_DOT + "expression.orderBy.isEmpty()",
" ORDER BY " + tableInfo.getKeyColumn(), orderBy);
}
+ // 情况二:加锁查询,只使用 Wrapper 的排序,末尾为 FOR UPDATE;首行为 LIMIT 1 FOR UPDATE
return SqlScriptUtils.convertIf(orderBy, "!" + WRAPPER_DOT + "expression.orderBy.isEmpty()", true)
+ NEWLINE + (kind == Kind.LOCK_FIRST ? "LIMIT 1 FOR UPDATE" : "FOR UPDATE");
}
+ // DONE @AI:方法内 注释;(处理:已补充同名 MappedStatement 跳过注入的注释,保留 flushCache、useCache 的说明)
@Override
protected MappedStatement addMappedStatement(Class> mapperClass, String id, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class> parameterType,
String resultMap, Class> resultType, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
+ // 已通过 XML、注解等定义同名的 MappedStatement 时,不重复注入
String statementName = mapperClass.getName() + DOT + id;
if (configuration.hasStatement(statementName, false)) {
logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
@@ -145,6 +157,7 @@ public class SelectForUpdateMethod extends AbstractMethod {
*
* MyBatis Plus Join 等插件复制 MappedStatement 时保留同一个 SqlSource,标记仍然有效
*/
+ @SuppressWarnings("ClassCanBeRecord")
@Getter
@AllArgsConstructor
public static class LockSqlSource implements SqlSource {
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index 18a093b..ad525f5 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -151,10 +152,6 @@ public interface BaseMapperX extends MPJBaseMapper {
return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3));
}
- // TODO DONE @AI:能不能把现有的 selectOneForUpdate、selectFirstOneForUpdate、selectListForFirstLock、 这些,做一个整合。目前感觉有点散。。。不然有点乱;包括 cms 项目里,其实也有一些 lock;/Users/yunai/Java/yudao-cms-all/ruoyi-vue-pro-jdk25/yudao-module-cms;最好有个比较万金油的方案。。。。
- // 已整合为 selectOneForUpdate、selectListForUpdate、selectFirstOneForUpdate 三个锁定读入口(含字段重载),boot、cms 的手写 FOR UPDATE 均改为调用它们;
- // 锁定子句由内置查询 SelectForUpdateMethod 生成(不再暴露 selectListForFirstLock 等接口方法),SQL Server 由 ForUpdateInnerInterceptor 改写,Oracle 的首行锁定在本类按数据库类型走多次查询。
-
// ========== 锁定读:需要在事务中调用;Wrapper 不能使用 last、groupBy、having、参数别名 ==========
/**
@@ -227,31 +224,40 @@ public interface BaseMapperX extends MPJBaseMapper {
* @param queryWrapper 查询条件
* @return 实体
*/
+ // DONE @AI:方法内注释噢;(处理:已按“情况一 非 Oracle 单条锁定、情况二 Oracle 2.1~2.3 候选、加锁、重读”补充步骤注释)
default T selectFirstOneForUpdate(LambdaQueryWrapper queryWrapper) {
+ // 情况一:非 Oracle,单条 LIMIT 1 FOR UPDATE 锁定首行(SQL Server 由 ForUpdateInnerInterceptor 改写)
DbType dbType = JdbcUtils.getDbType();
- if (!ObjectUtils.equalsAny(dbType, DbType.ORACLE, DbType.ORACLE_12C)) {
+ // DONE @AI:ObjectUtils notequals any;(处理:已改为 ObjectUtils.notEqualsAny,dbType 为 null 时与原取反写法一致,均走单条锁定)
+ if (ObjectUtils.notEqualsAny(dbType, DbType.ORACLE, DbType.ORACLE_12C)) {
return CollUtil.getFirst(selectListByForUpdateMethod(SelectForUpdateMethod.Kind.LOCK_FIRST, queryWrapper, null));
}
+
+ // 情况二:Oracle,按“查询候选主键、按主键加锁、重新读取首行主键”重试,最多 5 次
int maxAttempts = 5;
for (int i = 0; i < maxAttempts; i++) {
- List