問題
條件:MYSQL數(shù)據(jù)庫使用讀寫分離代理晾虑,例如使用阿里云的RDS+主從分離代理
對(duì)于一個(gè)簡(jiǎn)單的Insert偏形,獲取不到主鍵ID,getId()返回0阻星。
下面是常見的寫法:
<insert id="insert" parameterType="com.github.slankka.domain.model.DalaoTest">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
SELECT LAST_INSERT_ID()
</selectKey>
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>
接下來是
<insert id="insert" keyColumn="id"
keyProperty="id" parameterType="com.github.slankka.domain.model.DalaoTest" useGeneratedKeys="true">
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>
區(qū)別
這兩者都是獲取主鍵的寫法。
第一種
當(dāng)使用SelectKey時(shí)已添,Mybatis會(huì)使用SelectKeyGenerator妥箕,INSERT之后滥酥,多發(fā)送一次查詢語句,獲得主鍵值畦幢,在上述讀寫分離被代理的情況下坎吻,會(huì)得不到正確的主鍵。
第二種
當(dāng)MapperXML使用 useGeneratedKeys=true 不寫SelectKey節(jié)點(diǎn)呛讲,且當(dāng)Mybatis的配置中開啟useGeneratedKeys時(shí)禾怠,Mybatis會(huì)使用Jdbc3KeyGenerator, (可以參考下面parseStatementNode的注釋)
但需要注意的是,當(dāng)主鍵不是id的時(shí)候贝搁,需要定義
keyColumn="anotherId" keyProperty="anotherId"
使用該KeyGenerator的好處就是直接在一次INSERT 語句內(nèi),通過resultSet獲取得生成的主鍵值芽偏,并很好的支持設(shè)置了讀寫分離代理的數(shù)據(jù)庫雷逆,例如阿里云RDS + 讀寫分離代理,無需指定主庫污尉。
原理
//org.apache.ibatis.builder.xml.XMLStatementBuilder
public void parseStatementNode() {
//省略其他代碼
//這里處理所有的SelectKey的節(jié)點(diǎn)膀哲,并存儲(chǔ)KeyGenerator
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
//這里如果找到了KeyGenerator 則,忽略配置文件的useGeneratedKeys被碗。
//如果XML不寫selectKey某宪,原來他是自動(dòng)根據(jù)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);
}
最后兴喂,在執(zhí)行INSERT之后,調(diào)用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 的區(qū)別衣迷,可以查看源碼。
解決方案
根據(jù)上文所述酱酬,讓Mybatis 為INSERT或者UPDATE 使用Jdbc3KeyGenerator的方法就是壶谒,不寫SelectKey,并且開啟useGeneratedKeys=true膳沽。
<setting name="useGeneratedKeys" value="true"/>
另外一種臨時(shí)方案:
就是用攔截器讓SelectKey的語句汗菜,強(qiáng)制訪問主庫。
@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);
//阿里云讀寫分離強(qiáng)制指定主庫方案: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()
}
}