问题
条件:MYSQL数据库使用读写分离代理,例如使用阿里云的RDS+主从分离代理
对于一个简单的Insert,获取不到主键ID,getId()返回0。
下面是常见的写法:
SELECT LAST_INSERT_ID()
insert into dalao_test (name, gender, create_time,
update_time, status)
values (#{name,jdbcType=VARCHAR}, #{gender,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP},
#{updateTime,jdbcType=TIMESTAMP}, #{status,jdbcType=INTEGER})
接下来是
insert into dalao_test (name, gender, create_time,
update_time, status)
values (#{name,jdbcType=VARCHAR}, #{gender,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP},
#{updateTime,jdbcType=TIMESTAMP}, #{status,jdbcType=INTEGER})
区别
这两者都是获取主键的写法。
第一种
当使用SelectKey时,Mybatis会使用SelectKeyGenerator,INSERT之后,多发送一次查询语句,获得主键值,在上述读写分离被代理的情况下,会得不到正确的主键。
第二种
当MapperXML使用 useGeneratedKeys=true 不写SelectKey节点,且当Mybatis的配置中开启useGeneratedKeys时,Mybatis会使用Jdbc3KeyGenerator, (可以参考下面parseStatementNode的注释)
但需要注意的是,当主键不是id的时候,需要定义
keyColumn="anotherId" keyProperty="anotherId"
使用该KeyGenerator的好处就是直接在一次INSERT 语句内,通过resultSet获取得生成的主键值,并很好的支持设置了读写分离代理的数据库,例如阿里云RDS + 读写分离代理,无需指定主库。
原理
//org.apache.ibatis.builder.xml.XMLStatementBuilder
public void parseStatementNode() {
//省略其他代码
//这里处理所有的SelectKey的节点,并存储KeyGenerator
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
//这里如果找到了KeyGenerator 则,忽略配置文件的useGeneratedKeys。
//如果XML不写selectKey,原来他是自动根据useGeneratedKeys 就使用了Jdbc3KeyGenerator。
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
//凡是写了selectKey的全部都是selectKeyGenerator。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
最后,在执行INSERT之后,调用processAfter。
public interface KeyGenerator {
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
具体Jdbc3KeyGenerator和selectKeyGenerator 的区别,可以查看源码。
解决方案
根据上文所述,让Mybatis 为INSERT或者UPDATE 使用Jdbc3KeyGenerator的方法就是,不写SelectKey,并且开启useGeneratedKeys=true。
另外一种临时方案:
就是用拦截器让SelectKey的语句,强制访问主库。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MyBatisPlugin implements Interceptor{
private static final Logger logger = LoggerFactory.getLogger(MyBatisPlugin.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
Object objects = (Object)invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(objects);
//阿里云读写分离强制指定主库方案:https://help.aliyun.com/knowledge_detail/52221.html
if ((boundSql.getSql().contains("LAST_INSERT_ID") || StringUtils.contains(mappedStatement.getId(), "selectKey")) && !boundSql.getSql().contains("FORCE_MASTER")) {
SqlSource sqlSource = mappedStatement.getSqlSource();
if (sqlSource instanceof RawSqlSource) {
Class extends RawSqlSource> aClass = ((RawSqlSource) sqlSource).getClass();
Field sqlField = aClass.getDeclaredField("sqlSource");
sqlField.setAccessible(true);
Object staticSqlSource = sqlField.get(sqlSource);
if (staticSqlSource instanceof StaticSqlSource) {
Class extends StaticSqlSource> rawSqlSource = ((StaticSqlSource) staticSqlSource).getClass();
Field sqlInStatic = rawSqlSource.getDeclaredField("sql");
sqlInStatic.setAccessible(true);
String sqlStr = (String) sqlInStatic.get(staticSqlSource);
sqlInStatic.set(staticSqlSource, "/*FORCE_MASTER*/ " + sqlStr);
}
}
}
return invocation.proceed()
}
}