likes
comments
collection
share

水煮MyBatis(二三)- 关于ID策略的"BUG"

作者站长头像
站长
· 阅读数 36

前言

之所以不在一篇里写完,实在是不擅长写长文,写写停停,断断续续,时间跨度越长,越是磨人。

上一篇中,有意忽略了@GeneratedValue注解里的generator参数,主要有两层原因:

  • 使用频率不高;
  • 与预期效果有差异;

来个bug

首先设定id的generator为数据库生成的随机数

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "select round(rand()*(99-10) );")
    private Integer id;

测试用例

在用例里面,执行完成insert语句之后,再执行一次SELECT LAST_INSERT_ID(),用来获取数据库中真正生成的id。

    @Test
    public void generateBug() {
        ImageInfo info = new ImageInfo();
        info.setMd5(Digests.randomMd5());
        info.setFirstJobId(12);
        info.setImgUrl("https://www.test.com/img/123123123.jpg");
        // 是否成功的标志,成功:1,失败:0
        int flag = imageInfoMapper.insertSelective(info);
        log.info(">>>>>>>>>>>>>>>>>>>> id:{},success:{}", info.getId(), flag == 1);
        log.info(">>>>>>>>>>>>>>>>>>>> lastInsertId:{}", imageInfoMapper.lastInsertId());
    }

lastInsertId()方法在上文的末尾有提到。

日志

我们可以看到,获取随机值的SQL被执行了,而两行日志打印的id并不一样。在数据库中,自增生成的id是18,实体类中id的值是14,那么问题出在哪里呢?

Creating a new SqlSession
JDBC Connection [HikariProxyConnection@257650296 wrapping com.mysql.cj.jdbc.ConnectionImpl@4b4eced1] will not be managed by Spring
==>  Preparing: INSERT INTO tb_image ( id,md5,img_url,first_job_id ) VALUES( ?,?,?,? )
==> Parameters: null, 15954be87925bab5197ab6d330b40659(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)
<==    Updates: 1
==>  Executing: select round(rand()*(99-10) );
<==    Columns: round(rand()*(99-10) )
<==        Row: 14.0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6850b758]
13:52:10.406 logback [main] INFO  c.e.mybatis.MybatisApplicationTests - >>>>>>>>>>>>>>>>>>>> id:14,success:true
Creating a new SqlSession
JDBC Connection [HikariProxyConnection@1146045637 wrapping com.mysql.cj.jdbc.ConnectionImpl@4b4eced1] will not be managed by Spring
==>  Preparing: SELECT LAST_INSERT_ID()
==> Parameters: 
<==    Columns: LAST_INSERT_ID()
<==        Row: 18
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65d8dff8]
13:52:10.415 logback [main] INFO  c.e.mybatis.MybatisApplicationTests - >>>>>>>>>>>>>>>>>>>> lastInsertId:18

分析

让我们把视线聚焦到日志里的这一行:

==>  Preparing: INSERT INTO tb_image ( id,md5,img_url,first_job_id ) VALUES( ?,?,?,? )
==> Parameters: null, 15954be87925bab5197ab6d330b40659(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)

可以据此推理一下执行过程:

  • insert执行时,id参数是null;
  • 在insert执行完成以后,立刻执行select round(rand()*(99-10) );,生成一个随机值;
  • 将生成的随机值,赋给实体类的id属性;

说明在执行insert之前,我们通过SQL生成的随机值,没有被作为一个有效参数。而这两个操作是割裂的,完全没有什么关联性。

理想情况

理论上,如果要将sql生成的随机值,作为id保存到数据库里的前提条件,是在执行insert之前,将此数值作为insert语句参数一起执行。 比如上一篇中提到的,使用Java生成id:

    @Id
    @KeySql(genId = GenerateId.class)
    private Integer id;

日志如下,可以看到id作为参数,传入了insert语句。

==>  Preparing: INSERT INTO tb_image ( id,md5,img_url,first_job_id ) VALUES( ?,?,?,? )
==> Parameters: 1884980(Integer), 466747883fc3b47a5175c6dc60bfec66(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)
<==    Updates: 1

关键源码

上一篇中提到,SelectKeyGenerator里有两个重要方法:

  • processBefore:在insert执行之前,处理id属性;
  • processAfter:在insert执行之后,处理id属性;

关键的地方也在这里了,如果想根据我们设定的方式来生成id,一定要在processBefore里生成id,将之设置到对象id属性里,当成一个有效参数,一起参与insert操作。 水煮MyBatis(二三)- 关于ID策略的"BUG"

正确姿势

是说使用【注解 + SQL】生成ID的正确姿势,否则上面提到的Java生成策略:@KeySql(genId = GenerateId.class),也是一个选择。

方式一

使用 @SelectKey注解,imageInfo对象中没有设置id属性,在processBefore方法中根据设定的SQL语句生成。

    @Insert("insert into tb_image (id,md5,first_job_id,img_url) " +
            " values(#{id},#{md5},#{firstJobId},#{imgUrl})")
    @SelectKey(statement = "select round(rand()*(99-10) );", keyProperty = "id", before = true, resultType = int.class)
    int insertBySelectKey(ImageInfo imageInfo);

注意此处的before属性,一定要设置为true,才能满足processBefore的执行条件。 在MapperAnnotationBuilder的handleSelectKeyAnnotation方法中,executeBefore的值完全取决于before的值。

    boolean executeBefore = selectKeyAnnotation.before();
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
    configuration.addKeyGenerator(id, answer);

日志

从日志中可以看出,在执行insert之前,执行了我们配置的获取id的SQL。

JDBC Connection [HikariProxyConnection@43473566 wrapping com.mysql.cj.jdbc.ConnectionImpl@765ffb14] will not be managed by Spring
==>  Preparing: select round(rand()*(99-10) );
==> Parameters: 
<==    Columns: round(rand()*(99-10) )
<==        Row: 54.0
<==      Total: 1
==>  Preparing: insert into tb_image (id,md5,first_job_id,img_url) values(?,?,?,?)
==> Parameters: 54(Integer), 13979669113c78ae14237905e12f1d7f(String), 12(Integer), https://www.test.com/img/123123123.jpg(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4207609e]
15:37:20.708 logback [main] INFO  c.e.mybatis.MybatisApplicationTests - >>>>>>>>>>>>>>>>>>>> id:54,success:true

方式二

使用tk框架的@KeySql注解,tk推荐优先使用此注解来处理id的一切策略选择。

    @Id
    @KeySql(sql = "select round(rand()*(99-10) );", order = ORDER.BEFORE)
    private Integer id;

和@SelectKey一样,也是需要注意触发processBefore的执行条件。在源码中,executeBefore的条件,取决于order的值。

            MappedStatement keyStatement = configuration.getMappedStatement(keyId, false);
            //如果单独设置了 order,使用 column 提供的,否则使用全局的
            keyGenerator = new SelectKeyGenerator(keyStatement, column.getOrder() != ORDER.DEFAULT ? (column.getOrder() == ORDER.BEFORE) : executeBefore);
            configuration.addKeyGenerator(keyId, keyGenerator);

日志

从日志中可以看出,在执行insert之前,执行了我们配置的获取id的SQL。

JDBC Connection [HikariProxyConnection@2050360660 wrapping com.mysql.cj.jdbc.ConnectionImpl@424de326] will not be managed by Spring
==>  Executing: select round(rand()*(99-10) );
<==    Columns: round(rand()*(99-10) )
<==        Row: 86.0
<==      Total: 1
==>  Preparing: INSERT INTO tb_image ( id,md5,img_url,first_job_id ) VALUES( ?,?,?,? )
==> Parameters: 86(Integer), bbd05d391078352776366f1063c97002(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1f77b5cc]
15:41:16.457 logback [main] INFO  c.e.mybatis.MybatisApplicationTests - >>>>>>>>>>>>>>>>>>>> id:86,success:true