2018-06-07 Mybatis 遇到MYSQL读写分离获取主键的问题

问题

条件: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 aClass = ((RawSqlSource) sqlSource).getClass();
                Field sqlField = aClass.getDeclaredField("sqlSource");
                sqlField.setAccessible(true);
                Object staticSqlSource = sqlField.get(sqlSource);
                if (staticSqlSource instanceof StaticSqlSource) {
                    Class 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()
        }
}

你可能感兴趣的:(2018-06-07 Mybatis 遇到MYSQL读写分离获取主键的问题)