主键生成策略:自增、selectKey 与 UUID/雪花
数据表都要有主键,但"谁来生成主键"容易被含糊带过:是数据库自增,还是应用生成?选择不同,插入后能否拿回主键、分布式下是否冲突,结果完全不同。本节梳理自增、selectKey、UUID 与雪花四种思路及推荐用法。
主键来源只有两类
- 数据库生成:AUTO_INCREMENT(MySQL)、序列(Oracle)、数据库函数等,插入时自动赋值。
- 应用生成:程序里用 UUID、雪花算法等算好值再 INSERT,不依赖数据库。
MyBatis 本身不生成主键,只负责把数据库生成的值回填到实体,或用你传入的值执行插入。
自增主键:useGeneratedKeys
自增主键在 MySQL 最常见,插入后要用 useGeneratedKeys + keyProperty 把数据库回填的 id 写进实体属性:
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, email)
VALUES (#{name}, #{email})
</insert>
接口方法把实体传进去,插入后 id 已被回填:
User user = new User();
user.setName("小明");
userMapper.insert(user);
System.out.println(user.getId()); // 输出:数据库回填的自增 id,如 1
要点:useGeneratedKeys 依赖 JDBC 驱动支持返回生成键(MySQL 支持);keyProperty 写实体属性名而非列名;批量插入时部分数据库只回填第一条记录,具体以官方文档为准。
selectKey:先从数据库取主键
Oracle 序列、数据库函数生成 key 的场景,用 <selectKey> 先查再插或插后回查:
<insert id="insertUser">
<selectKey keyProperty="id" resultType="string" order="BEFORE">
SELECT REPLACE(UUID(), '-', '') -- MySQL 生成 32 位 UUID
</selectKey>
INSERT INTO user (id, name) VALUES (#{id}, #{name})
</insert>
order="BEFORE":先执行 selectKey 再 INSERT,适合 UUID、序列等"插入前就要值"的场景;order="AFTER":先 INSERT 再查生成键,适合驱动不支持 useGeneratedKeys 的数据库;- Oracle 的典型写法是
SELECT seq_user.NEXTVAL FROM DUAL,各数据库语句以官方文档为准。
UUID 与雪花:应用生成
UUID:java.util.UUID 直接生成,全局唯一、不依赖数据库;缺点是无序且偏长,做主键会拉低 InnoDB 聚簇索引的写入与查询性能:
user.setId(UUID.randomUUID().toString().replace("-", "")); // 32 位十六进制字符串
雪花算法(Snowflake):64 位 long,由时间戳 + 机器标识 + 序列号拼成,趋势递增、无中心依赖,适合分布式。实现细节多,业界普遍直接用现成组件,例如 MyBatis-Plus 的 IdType.ASSIGN_ID 即内置雪花实现(用法以官方文档为准):
@TableId(type = IdType.ASSIGN_ID) // MP:id 由雪花算法自动生成
private Long id;
如何选择
| 场景 | 推荐 | 理由 |
|---|---|---|
| 单库单体、不分表 | 自增 | 简单、有序、索引友好 |
| 需插入前知道 ID、客户端生成 | UUID | 无中心依赖 |
| 分布式、高并发写 | 雪花(ASSIGN_ID) | 趋势递增、全局唯一 |
自增适合单库,UUID 与雪花面向分布式。数据库侧回填用 keyProperty,应用侧生成靠 IdType,两条路各掌握一种,遇到不熟悉的数据库以官方文档为准。