likes
comments
collection
share

水煮MyBatis(二二)- 细说一下ID生成策略

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

前言

数据库表唯一值的生成策略,一开始就备受关注,从自增到UUID,从单机到分布式,业务千奇百怪,策略也是层出不穷。

锲子

在Mybatis框架里,假定表使用了【AUTO_INCREMENT】策略,在通过【mapper.insertSelective(info)】语句写入一条记录的同时,会将对应id的值,写到info对象的id属性里,这是怎么做到的?

ID生成

有句话得说在前面,对于id自增的表来说,id生成策略是没有太大实际意义的;常规开发场景中,要么使用自增,要么使用代码根据一定规则生成,将两者混合使用的情况比较少见。

在Mybatis里,常用的ID生成注解有两个,其中@GeneratedValue注解应该是使用最广泛的,这里也以此注解展开。

  • @GeneratedValue,这是java内置的注解
  • @KeySql,tk扩展的注解

关键源码

对于锲子中的案例,其实是数据库自增生成了id,然后mybatis使用IDENTITY对应的策略,将id查询返回,并设置到类实体的id属性中。

SelectKeyGenerator

在此类中,获取自增生成的id,并将其设置到类实体的id属性中。processBefore和processAfter,只有一个会执行,最终的处理逻辑【简化版】:

    private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
      String[] keyProperties = keyStatement.getKeyProperties();
      final Configuration configuration = ms.getConfiguration();
      final MetaObject metaParam = configuration.newMetaObject(parameter);
      if (keyProperties != null) {
          // Do not close keyExecutor.
          // The transaction will be closed by parent executor.
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
          // 执行查询SELECT LAST_INSERT_ID()
          List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
          // 执行语句:metaParam.setValue(property, value);
          setValue(metaParam, keyProperties[0], values.get(0));
      }
    }

执行写入

processAfter就是上面提到的方法,更新【写入】完成以后,对id进行处理。processBefore方法在下一篇中进行介绍,会有一些意想不到的表现。

  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    // 查询最新生成的id,并设置到对象id属性
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

@GeneratedValue介绍

沿用锲子里的案例,id自增,常用配置如下:

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

strategy的选择

  • TABLE,Mybatis不支持,一般与@TableGenerator注解搭配使用,通过数据库表来为id生成唯一值,配置方式:GeneratedValue(strategy=TABLE, generator="addressGen")
  • SEQUENCE,序列号,Mybatis不支持,在Oracle数据库支持的比较好,如果mysql要用此配置,需要用表的方式来生成序列号;一般与@SequenceGenerator注解搭配使用,配置方式:@SequenceGenerator(name="EMP_SEQ", allocationSize=25)
  • IDENTITY,常用配置,使用默认数据库方言语句来获取id;
  • AUTO,自动,一般不使用,配置方式:@GeneratedValue(strategy = GenerationType.AUTO, generator = "JDBC")
tk.mybatis.mapper.MapperException: id - 该字段@GeneratedValue配置只允许以下几种形式:
1.useGeneratedKeys的@GeneratedValue(generator="JDBC")  
2.类似mysql数据库的@GeneratedValue(strategy=GenerationType.IDENTITY[,generator="Mysql"])

从此报错信息来看,可供我们选择的方式是非常有限的,初始化的源码如下:

IDENTITY策略对应的方言

    DB2("VALUES IDENTITY_VAL_LOCAL()"),
    MYSQL("SELECT LAST_INSERT_ID()"),
    SQLSERVER("SELECT SCOPE_IDENTITY()"),
    CLOUDSCAPE("VALUES IDENTITY_VAL_LOCAL()"),
    DERBY("VALUES IDENTITY_VAL_LOCAL()"),
    HSQLDB("CALL IDENTITY()"),
    SYBASE("SELECT @@IDENTITY"),
    DB2_MF("SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1"),
    INFORMIX("select dbinfo('sqlca.sqlerrd1') from systables where tabid=1"),
    DEFAULT(""),
    NULL("");

使用Java生成ID

普通

最简单也直白的方式,莫过于直接设置了。类似于下面这行代码,在getImageId()方法中,我们可以使用各种方式来生成。 image.setId(Idgenerate.getImageId())

卷式写法

tk.mybatis中,留有一个入口:【GenId接口】,支持针对不同的表来设定特定的生成策略。和上面的方式一样,不局限于id字段。

@Table(name = "tb_image")
public class ImageInfo implements Serializable {

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

// 生成id代码
public class GenerateId implements GenId<Integer> {

    @Override
    public Integer genId(String table, String column) {
        return (int) System.currentTimeMillis() / 1000;
    }
}

例子

实体类

@Table(name = "tb_image")
public class ImageInfo implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    ...
}

单元测试

    @Test
    public void insert() {
        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);
    }

输出

JDBC Connection [HikariProxyConnection@1336418989 wrapping com.mysql.cj.jdbc.ConnectionImpl@2fcd7d3f] will not be managed by Spring
==>  Preparing: INSERT INTO tb_image ( id,md5,img_url,first_job_id ) VALUES( ?,?,?,? )
==> Parameters: null, 760e830a6f002251e1b43fd74fb2feb1(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)
<==    Updates: 1
==>  Executing: SELECT LAST_INSERT_ID()
<==    Columns: LAST_INSERT_ID()
<==        Row: 13
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2687725a]

从输出日志中可以看到,执行了sql语句【SELECT LAST_INSERT_ID()】

LAST_INSERT_ID有并发问题吗

比如说,在高并发写入时,A事务获取到B事务执行以后的LAST_INSERT_ID,毕竟此函数的字面意思是最后插入的id。 看看官方文档对此函数的介绍:

Introduction to MySQL LAST_INSERT_ID() function The LAST_INSERT_ID() function returns the first automatically generated integer ( BIGINT UNSIGNED) successfully inserted for an AUTO_INCREMENT column. If you insert multiple rows into the table using a single INSERT statement, the LAST_INSERT_ID() function returns the first automatically generated value only. If the insertion fails, the result returned by the LAST_INSERT_ID() remain unchanged. The LAST_INSERT_ID() function works based on client-independent principle. It means the value returned by the LAST_INSERT_ID() function for a specific client is the value generated by that client only to ensure that each client can obtain its own unique ID.

大概意思是说,LAST_INSERT_ID()函数为每个客户端返回的值是当前客户端生成的值,保证每个客户端都可以获得自己的唯一ID;这样就可以有效避免前面说的并发问题,毕竟已经做到了客户端隔离。

这里提一句,客户端隔离,在代码中的体现形式,在我看来应该是mysql链接,一个链接代表了一个客户端。

能否删除@GeneratedValue注解

可以的,因为在我们的例子中,ID是mySQL自增生成的,没有使用自定义的生成策略,如果删除此注解,对于此例子而言,只是没有返回数据生成的id数值而已。在下面的日志中,可以看到没有Executing: SELECT LAST_INSERT_ID(),所以打印的id是null。

JDBC Connection [HikariProxyConnection@1084339924 wrapping com.mysql.cj.jdbc.ConnectionImpl@175581eb] will not be managed by Spring
==>  Preparing: INSERT INTO tb_image ( md5,img_url,first_job_id ) VALUES( ?,?,? )
==> Parameters: 2bcbd15e2d779ac7fa3926c75df6ef35(String), https://www.test.com/img/123123123.jpg(String), 12(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77b5148c]
14:38:12.822 logback [main] INFO  c.e.mybatis.MybatisApplicationTests - >>>>>>>>>>>>>>>>>>>> id:null,success:true

当然,LAST_INSERT_ID也可以独立执行,比如在Mapper里定义这么一个方法,在执行insert之后,可以查询到当前客户端最近写入操作的id。

    @Select("SELECT LAST_INSERT_ID()")
    int lastInsertId();