MyBatis主键回填的两种实现方式
作者:身如柳絮随风扬
购物下单后,订单号是怎么自动生成的?插入数据后如何直接获取自增主键?本文将带你深入理解 MyBatis 中的主键回填机制,并通过流程图和代码示例彻底掌握它。
一、业务场景:下单后如何返回订单编号?
想象一个典型的购物交易流程:
- 用户点击“立即购买”,填写收货信息,提交订单。
- 后端服务将订单数据(用户ID、商品ID、金额、状态等)插入到数据库的
orders表中。 - 插入成功后,系统需要立即返回一个订单编号给前端,用于后续支付、查询等操作。
这个订单编号通常是数据库表的主键,比如自增的 id 字段。但在执行插入 SQL 时,我们并不知道这个 id 是多少 —— 它是数据库自动生成的。
如果采用“先插入,再查询”的方式:
-- 第一步:插入订单(id 自动生成) INSERT INTO orders(user_id, amount, status) VALUES(1001, 299.00, 0); -- 第二步:根据唯一业务字段查询刚插入的订单 SELECT id FROM orders WHERE user_id = 1001 AND create_time = ...;
这种做法不仅繁琐,而且在高并发下很容易出错(多个用户同时下单,条件可能不唯一)。于是,主键回填技术应运而生。
二、什么是主键回填?
主键回填(Key Fillback / Key Return)是指在执行数据库插入操作后,自动将生成的主键值填充到传入的 Java 对象中。这样,你无需再次查询,就能直接通过对象的 getId() 方法拿到主键。
简单来说:插入时主键为 null,插入后 MyBatis 帮你把主键值“填回去”。
效果演示
Order order = new Order(); order.setUserId(1001); order.setAmount(299.00); order.setStatus(0); // 此时 order.getId() == null orderMapper.insertOrder(order); // 插入后,MyBatis 自动将生成的主键赋值给 order 对象 System.out.println(order.getId()); // 输出 12345
三、主键回填的两种实现方式
MyBatis 主要提供两种方式实现主键回填,分别适用于不同的数据库和场景。
方式一:useGeneratedKeys+keyProperty(推荐,简单高效)
适用数据库:支持自动生成主键的数据库,如 MySQL、SQL Server(自增列)、PostgreSQL(serial 类型)等。
原理:MyBatis 利用 JDBC 的 Statement.getGeneratedKeys() 方法获取数据库自动生成的主键。
配置示例:
<insert id="insertOrder" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders(user_id, amount, status)
VALUES (#{userId}, #{amount}, #{status})
</insert>useGeneratedKeys="true":开启主键回填功能。keyProperty="id":指定将生成的主键值赋给 Java 对象的哪个属性(对应Order类中的id字段)。
Java 接口方法:
public interface OrderMapper {
int insertOrder(Order order);
}使用注意:
- 只能用于单条插入(批量插入时需特殊处理,不同数据库支持情况不一)。
- 数据库表必须定义自增列或类似机制。
方式二:<selectKey>标签(灵活,支持多种数据库)
适用场景:
- 数据库没有自增列(如 Oracle 使用序列)。
- 需要在插入前生成主键(比如使用 UUID)。
- 需要执行自定义查询来获取主键值(如 MySQL 的
LAST_INSERT_ID())。
原理:通过子查询执行一个 SQL 语句来获取主键,可以指定在插入语句之前(order="BEFORE")或之后(order="AFTER")执行。
示例 1:MySQL 插入后获取自增 ID
<insert id="insertOrder">
<!-- 插入后执行,获取最后插入的 ID -->
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO orders(user_id, amount, status)
VALUES (#{userId}, #{amount}, #{status})
</insert>示例 2:Oracle 插入前从序列获取 ID
<insert id="insertOrder">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT ORDERS_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO orders(id, user_id, amount, status)
VALUES (#{id}, #{userId}, #{amount}, #{status})
</insert>示例 3:使用 UUID 作为主键(插入前生成)
<insert id="insertUser">
<selectKey keyProperty="id" resultType="String" order="BEFORE">
SELECT REPLACE(UUID(), '-', '')
</selectKey>
INSERT INTO users(id, name) VALUES (#{id}, #{name})
</insert>属性说明:
keyProperty:目标对象的属性名。resultType:主键的 Java 类型。order:BEFORE(在插入前执行)或AFTER(在插入后执行)。
四、流程图:主键回填的执行过程
为了更直观地理解两种方式的执行流程,下面用流程图表示。
1.useGeneratedKeys方式流程

2.<selectKey order="AFTER">方式流程(以 MySQL 为例)

3.<selectKey order="BEFORE">方式流程(以 Oracle 为例)

五、两种方式的对比与选择
| 对比项 | useGeneratedKeys | <selectKey> |
|---|---|---|
| 配置复杂度 | 极低,两个属性即可 | 稍高,需要写子查询 |
| 适用数据库 | 支持自增列的数据库(MySQL 等) | 所有数据库(Oracle、SQL Server 等) |
| 性能 | 高,一次网络交互 | 可能两次交互(AFTER 时) |
| 批量插入支持 | 有限制(需看驱动) | 一般不用于批量 |
| 主键生成时机 | 插入后 | 可自由控制(BEFORE / AFTER) |
| 非数值主键(UUID) | 不支持 | 支持 |
选择建议:
- MySQL + 自增主键 → 优先使用
useGeneratedKeys。 - Oracle 序列 / UUID / 复合主键 → 使用
<selectKey>。 - 需要插入前生成主键(如分布式 ID) → 使用
<selectKey order="BEFORE">。
六、常见问题与注意事项
1. 为什么插入后对象的 id 还是 null?
- 检查
useGeneratedKeys="true"和keyProperty是否正确配置。 - 检查
keyProperty对应的属性在 Java 对象中是否有 setter 方法(MyBatis 通过反射赋值)。 - 确认数据库表确实支持自增列(MySQL 需
AUTO_INCREMENT)。
2. 批量插入时能否使用主键回填?
- MySQL 驱动支持批量插入时返回生成的主键,但 MyBatis 的
useGeneratedKeys对批量支持不统一。建议使用循环单条插入,或者使用<foreach>配合特殊写法(需要数据库驱动支持)。 - 简单场景下,可以放弃批量回填,插入后单独查询。
3.LAST_INSERT_ID()与并发
MySQL 的 LAST_INSERT_ID() 是连接级别的,每个客户端连接返回自己最后插入的 ID,不会受其他并发连接影响,因此是安全的。
4. 主键回填后,对象会被修改吗?
会!MyBatis 会直接修改传入的 Java 对象的属性值。因此你无需重新接收返回值,直接使用原对象即可。
七、完整示例代码
数据库表(MySQL)
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `amount` decimal(10,2) NOT NULL, `status` tinyint(4) DEFAULT '0', PRIMARY KEY (`id`) );
Java 实体类
public class Order {
private Integer id;
private Integer userId;
private BigDecimal amount;
private Integer status;
// 省略 getter / setter / toString
}Mapper 接口
@Mapper
public interface OrderMapper {
// 方式一:useGeneratedKeys
int insertOrder(Order order);
// 方式二:selectKey (MySQL AFTER)
int insertOrderWithSelectKey(Order order);
}XML 映射文件
<!-- 方式一 -->
<insert id="insertOrder" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders(user_id, amount, status)
VALUES (#{userId}, #{amount}, #{status})
</insert>
<!-- 方式二 -->
<insert id="insertOrderWithSelectKey">
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO orders(user_id, amount, status)
VALUES (#{userId}, #{amount}, #{status})
</insert>测试代码
@SpringBootTest
class OrderMapperTest {
@Autowired
private OrderMapper orderMapper;
@Test
void testInsert() {
Order order = new Order();
order.setUserId(1001);
order.setAmount(new BigDecimal("199.00"));
order.setStatus(0);
int rows = orderMapper.insertOrder(order);
System.out.println("影响行数:" + rows);
System.out.println("回填后的主键:" + order.getId()); // 输出自增 ID
}
}八、总结
主键回填是 MyBatis 中非常实用且高频使用的特性,它解决了插入数据后立即获取主键的痛点。掌握两种实现方式:
useGeneratedKeys:简洁高效,MySQL 首选。<selectKey>:灵活强大,适配各种数据库及非自增主键场景。
在实际开发中,根据数据库类型和业务需求选择合适的方式,可以大大简化代码逻辑,提升开发效率。
以上就是MyBatis主键回填的两种实现方式的详细内容,更多关于MyBatis实现主键回填的资料请关注脚本之家其它相关文章!
