首先主键策略和数据库关系很大,有些数据库支持主键自增,而有些数据库只能通过序列来获得。
新增的@KeySql
注解用于替换 @GeneratedValue
注解,因此 @KeySql
能以更简单方式实现原来的功能,下面的示例都先使用 @KeySql
进行配置,然后在使用 @GeneratedValue
,大家可以自己选择。
JDBC 支持通过 getGeneratedKeys
方法取回主键的情况
这种情况首先需要数据库支持自增,其次数据库提供的 JDBC 支持 getGeneratedKeys
方法。
常见的如 MySql,SqlServer 支持这种模式。
这种情况下,配置主键策略最简单。
用法如下:
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
或:
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
为了让大家容易理解这里配置和 MyBatis 写法的关系,大家可以看看对应生成的 XML 代码:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into country (id, countryname, countrycode)
values (#{id},#{countryname},#{countrycode})
</insert>
注意:SqlServer 中使用时,需要设置 id 的 insertable=false
支持自增的数据库列表如下:
DB2 | VALUES IDENTITY_VAL_LOCAL() |
MYSQL | SELECT LAST_INSERT_ID() |
SQLSERVER | SELECT SCOPE_IDENTITY() |
CLOUDSCAPE | VALUES IDENTITY_VAL_LOCAL() |
DERBY | 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 |
这类数据库主键策略配置示例如下:
@Id
//DEFAULT 需要配合 IDENTITY 参数(ORDER默认AFTER)
@KeySql(dialect = IdentityDialect.DEFAULT)
private Integer id;
//建议直接指定数据库
@Id
@KeySql(dialect = IdentityDialect.MYSQL)
private Integer id;
或:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@GeneratedValue
用法还要配合 IDENTITY 参数(以及 ORDER 参数),这个参数值需要配置为对应的数据库,就是上面列表中的名字
mapper.identity=mysql
mapper.order=AFTER
//或者
#mapper.before=false
这种配置对应的 XML 形式为:
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into country (id, countryname, countrycode)
values (#{id},#{countryname},#{countrycode})
</insert>
IDENTITY 参数以及 ORDER 参数会决定 selectKey
中的 SQL 和 order
属性的值(这里是 AFTER
)。
通过序列和任意 SQL 获取主键值
像 Oracle 中通过序列获取主键就属于这种情况,除了类似序列获取值外,还可以是获取 UUID 的 SQL 语句,例如 select uuid()
。
mapper.identity=select uuid()
#mapper.identity=select seq.nextval()
mapper.before=true
自增主键和非自增主键 的区别主要在于,自增主键 是插入表之后才有 id 的值,非自增主键是插入数据库前需要获取一个值作为主键。
在 Oracle 中,我们可以用下面的方式进行配置:
@Id
@KeySql(sql = "select SEQ_ID.nextval from dual", order = ORDER.BEFORE)
private Integer id;
或:
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY,
generator = "select SEQ_ID.nextval from dual")
private Integer id;
使用 @GeneratedValue
时也要配置一个 ORDER 全局参数
这种配置对应的 XML 代码如下:
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select SEQ_ID.nextval from dual
</selectKey>
insert into country (id, countryname, countrycode)
values (#{id},#{countryname},#{countrycode})
</insert>
这种用法中,values
中必须出现主键的值,否则就插不到数据库。
除了 Oracle 这种外,还有一种更普遍的用法,就是使用 UUID。
例如下面的配置:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,generator = "select uuid()")
private String id;
注意: SQL 返回值类型和这里接收的类型(String
)一致。
CREATE DEFINER=`ip`@`%` FUNCTION `sys_guid`() RETURNS varchar(255) CHARSET utf8
BEGIN
DECLARE uuid VARCHAR(32) DEFAULT '';
SET uuid = replace(uuid(),'-','');
RETURN uuid;
END
对于主键类型为varchar的,可以通过预定义的默认生成策略来生成主键(推荐)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
目前的生成策略为通过获得数据库生成的UUID来作为主键
mapper.identity=select sys_guid() from dual
同样对于varchar类型的主键,也可以直接使用系统生成的UUID,这个方式与GenerationType.IDENTITY的区别就是通过GenerationType.IDENTITY生成的主键能够在保存操作后,返回到实体中;而这种方式则不会
@GeneratedValue("UUID")
private String id;
@KeySql
介绍
上面的例子中都列举了 @KeySql
方式的用法。下面全面的看看这个注解:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface KeySql {
/**
* 是否使用 JDBC 方式获取主键,优先级最高,设置为 true 后,不对其他配置校验
*
* @return
*/
boolean useGeneratedKeys() default false;
/**
* 优先级第二,根据配置的数据库类型取回主键,忽略其他配置
*
* @return
*/
IdentityDialect dialect() default IdentityDialect.DEFAULT;
/**
* 取主键的 SQL
*
* @return
*/
String sql() default "";
/**
* 和 sql 可以配合使用,默认使用全局配置中的 ORDER
*
* @return
*/
ORDER order() default ORDER.DEFAULT;
}
通过上面的注释,大家可以看到主要的 3 个参数的优先级,useGeneratedKeys
优先级最高,其次是 dialect
,最后是 sql
。其中 order
只对 sql
方式有效。
useGeneratedKeys
和 dialect
相当于预设的基本用法,和数据库以及驱动紧密相关。sql
和 order
更灵活。
为了方便使用全局主键(例如:Vesta 是一款通用的ID产生器,互联网俗称统一发号器),通用 Mapper 4.0.2 版本增加了新的控制主键生成的策略。
@KeySql
注解增加了下面的方法:
/**
* Java 方式生成主键,可以和发号器一类的服务配合使用
*
* @return
*/
Class<? extends GenId> genId() default GenId.NULL.class;
使用该功能的时候,需要配置 genId
属性。 由于生成主键的方式通常和使用环境有关,因此通用 Mapper 没有提供默认的实现。
GenId
接口如下:
public interface GenId<T> {
class NULL implements GenId {
@Override
public Object genId(String table, String column) {
throw new UnsupportedOperationException();
}
}
T genId(String table, String column);
}
通过接口方法可以看出,在生成 Id 时,可以得到当前的表名和列名,我们可以使用这两个信息生成和表字段相关的 Id 信息。也可以完全不使用这两个信息,生成全局的 Id。
使用 UUID 方式
首先我们需要提供一个能产生 UUID 的实现类:
public class UUIdGenId implements GenId<String> {
@Override
public String genId(String table, String column) {
return UUID.randomUUID().toString();
}
}
使用:
public class User {
@Id
@KeySql(genId = UUIdGenId.class)
private String id;
}
注意:我们只需要在注解中配置 @KeySql(genId = UUIdGenId.class)
即可,需要注意的是,如果你使用了 @KeySql
提供的其他方式,genId
就不会生效,genId
是所有方式中优先级最低的。
简单的全局时序ID
public class SimpleGenId implements GenId<Long> {
private Long time;
private Integer seq;
@Override
public synchronized Long genId(String table, String column) {
long current = System.currentTimeMillis();
if (time == null || time != current) {
time = current;
seq = 1;
} else if (current == time) {
seq++;
}
return (time << 20) | seq;
}
}
基于 Vesta 的实现
public class VestaGenId implement GenId<Long> {
public Long genId(String table, String column){
//ApplicationUtil.getBean 需要自己实现
//IdService 是Vesta中的接口
IdService idService = ApplicationUtil.getBean(IdService.class);
return idService.genId();
}
}
@Component
@Lazy(false)
public class ApplicationContextRegister implements ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationContextRegister.class);
private static ApplicationContext APPLICATION_CONTEXT;
/**
* 设置spring上下文 * * @param applicationContext spring上下文 * @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
LOGGER.debug("ApplicationContext registed-->{}", applicationContext);
APPLICATION_CONTEXT = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return APPLICATION_CONTEXT;
}
}
InsertListMapper 特殊支持
通用 Mapper 提供了好几个 InsertListMapper
接口,最早提供的都是和数据库相关的方法,所以在 Id 策略上都有和数据库相关的限制。
最新增加的 tk.mybatis.mapper.additional.insert.InsertListMapper
接口是一个和数据库无关的方法,他不支持任何的主键策略。但是有了 genId
方式后,这个接口增加了对 @KeySql
注解 genId
方法的支持。
import tk.mybatis.mapper.additional.insert.InsertListMapper;
public interface UserMapper extends InsertListMapper<User> {
}
@Test
public void testInsertList() {
SqlSession sqlSession = getSqlSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = new ArrayList<User>(countries.length);
for (int i = 0; i < countries.length; i++) {
userList.add(new User(countries[i][0], countries[i][1]));
}
Assert.assertEquals(countries.length, mapper.insertList(userList));
for (User user : userList) {
Assert.assertNotNull(user.getId());
System.out.println(user.getId());
}
} finally {
sqlSession.close();
}
}
通过 genId
方式可以批量插入,并且可以回写 ID。
想要自己实现全局唯一id生成器可以参考:https://mp.youkuaiyun.com/console/editor/html/95203342
order="AFTER" mysql 自增长ID 先保存的数据 再生成的ID
order="BEFORE" oracle 自增长 UUID 先生成ID 再保存数据
order="BEFORE" mysql UUID 先生成ID 再保存数据