MyBatis 主键回填详解:从业务场景到两种实现方式
MyBatis 主键回填详解从业务场景到两种实现方式购物下单后订单号是怎么自动生成的插入数据后如何直接获取自增主键本文将带你深入理解 MyBatis 中的主键回填机制并通过流程图和代码示例彻底掌握它。一、业务场景下单后如何返回订单编号想象一个典型的购物交易流程用户点击“立即购买”填写收货信息提交订单。后端服务将订单数据用户ID、商品ID、金额、状态等插入到数据库的orders表中。插入成功后系统需要立即返回一个订单编号给前端用于后续支付、查询等操作。这个订单编号通常是数据库表的主键比如自增的id字段。但在执行插入 SQL 时我们并不知道这个id是多少 —— 它是数据库自动生成的。如果采用“先插入再查询”的方式-- 第一步插入订单id 自动生成INSERTINTOorders(user_id,amount,status)VALUES(1001,299.00,0);-- 第二步根据唯一业务字段查询刚插入的订单SELECTidFROMordersWHEREuser_id1001ANDcreate_time...;这种做法不仅繁琐而且在高并发下很容易出错多个用户同时下单条件可能不唯一。于是主键回填技术应运而生。二、什么是主键回填主键回填Key Fillback / Key Return是指在执行数据库插入操作后自动将生成的主键值填充到传入的 Java 对象中。这样你无需再次查询就能直接通过对象的getId()方法拿到主键。简单来说插入时主键为 null插入后 MyBatis 帮你把主键值“填回去”。效果演示OrderordernewOrder();order.setUserId(1001);order.setAmount(299.00);order.setStatus(0);// 此时 order.getId() nullorderMapper.insertOrder(order);// 插入后MyBatis 自动将生成的主键赋值给 order 对象System.out.println(order.getId());// 输出 12345三、主键回填的两种实现方式MyBatis 主要提供两种方式实现主键回填分别适用于不同的数据库和场景。方式一useGeneratedKeyskeyProperty推荐简单高效适用数据库支持自动生成主键的数据库如 MySQL、SQL Server自增列、PostgreSQLserial 类型等。原理MyBatis 利用 JDBC 的Statement.getGeneratedKeys()方法获取数据库自动生成的主键。配置示例insertidinsertOrderuseGeneratedKeystruekeyPropertyidINSERT INTO orders(user_id, amount, status) VALUES (#{userId}, #{amount}, #{status})/insertuseGeneratedKeystrue开启主键回填功能。keyPropertyid指定将生成的主键值赋给 Java 对象的哪个属性对应Order类中的id字段。Java 接口方法publicinterfaceOrderMapper{intinsertOrder(Orderorder);}使用注意只能用于单条插入批量插入时需特殊处理不同数据库支持情况不一。数据库表必须定义自增列或类似机制。方式二selectKey标签灵活支持多种数据库适用场景数据库没有自增列如 Oracle 使用序列。需要在插入前生成主键比如使用 UUID。需要执行自定义查询来获取主键值如 MySQL 的LAST_INSERT_ID()。原理通过子查询执行一个 SQL 语句来获取主键可以指定在插入语句之前orderBEFORE或之后orderAFTER执行。示例 1MySQL 插入后获取自增 IDinsertidinsertOrder!-- 插入后执行获取最后插入的 ID --selectKeykeyPropertyidresultTypeintorderAFTERSELECT LAST_INSERT_ID()/selectKeyINSERT INTO orders(user_id, amount, status) VALUES (#{userId}, #{amount}, #{status})/insert示例 2Oracle 插入前从序列获取 IDinsertidinsertOrderselectKeykeyPropertyidresultTypelongorderBEFORESELECT ORDERS_SEQ.NEXTVAL FROM DUAL/selectKeyINSERT INTO orders(id, user_id, amount, status) VALUES (#{id}, #{userId}, #{amount}, #{status})/insert示例 3使用 UUID 作为主键插入前生成insertidinsertUserselectKeykeyPropertyidresultTypeStringorderBEFORESELECT REPLACE(UUID(), -, )/selectKeyINSERT INTO users(id, name) VALUES (#{id}, #{name})/insert属性说明keyProperty目标对象的属性名。resultType主键的 Java 类型。orderBEFORE在插入前执行或AFTER在插入后执行。四、流程图主键回填的执行过程为了更直观地理解两种方式的执行流程下面用流程图表示。1.useGeneratedKeys方式流程调用 Mapper 方法传入对象 objMyBatis 解析 SQL 并创建 PreparedStatement执行插入 SQL数据库生成主键通过 Statement.getGeneratedKeys 获取主键将主键值设置到 obj 的 keyProperty 属性返回影响行数对象已包含主键2.selectKey orderAFTER方式流程以 MySQL 为例调用 Mapper 方法传入对象 obj执行 INSERT 语句数据入库执行 SELECT LAST_INSERT_ID 查询将查询结果设置到 obj 的 keyProperty返回影响行数对象已包含主键3.selectKey orderBEFORE方式流程以 Oracle 为例调用 Mapper 方法传入对象 obj执行 SELECT 序列.NEXTVAL 获取新主键将主键值设置到 obj 的 keyProperty执行 INSERT 语句使用 obj 中的主键值返回影响行数五、两种方式的对比与选择对比项useGeneratedKeysselectKey配置复杂度极低两个属性即可稍高需要写子查询适用数据库支持自增列的数据库MySQL 等所有数据库Oracle、SQL Server 等性能高一次网络交互可能两次交互AFTER时批量插入支持有限制需看驱动一般不用于批量主键生成时机插入后可自由控制BEFORE/AFTER非数值主键UUID不支持支持选择建议MySQL 自增主键 → 优先使用useGeneratedKeys。Oracle 序列 / UUID / 复合主键 → 使用selectKey。需要插入前生成主键如分布式 ID → 使用selectKey orderBEFORE。六、常见问题与注意事项1. 为什么插入后对象的 id 还是 null检查useGeneratedKeystrue和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 对象的属性值。因此你无需重新接收返回值直接使用原对象即可。七、完整示例代码数据库表MySQLCREATETABLEorders(idint(11)NOTNULLAUTO_INCREMENT,user_idint(11)NOTNULL,amountdecimal(10,2)NOTNULL,statustinyint(4)DEFAULT0,PRIMARYKEY(id));Java 实体类publicclassOrder{privateIntegerid;privateIntegeruserId;privateBigDecimalamount;privateIntegerstatus;// 省略 getter / setter / toString}Mapper 接口MapperpublicinterfaceOrderMapper{// 方式一useGeneratedKeysintinsertOrder(Orderorder);// 方式二selectKey (MySQL AFTER)intinsertOrderWithSelectKey(Orderorder);}XML 映射文件!-- 方式一 --insertidinsertOrderuseGeneratedKeystruekeyPropertyidINSERT INTO orders(user_id, amount, status) VALUES (#{userId}, #{amount}, #{status})/insert!-- 方式二 --insertidinsertOrderWithSelectKeyselectKeykeyPropertyidresultTypeintorderAFTERSELECT LAST_INSERT_ID()/selectKeyINSERT INTO orders(user_id, amount, status) VALUES (#{userId}, #{amount}, #{status})/insert测试代码SpringBootTestclassOrderMapperTest{AutowiredprivateOrderMapperorderMapper;TestvoidtestInsert(){OrderordernewOrder();order.setUserId(1001);order.setAmount(newBigDecimal(199.00));order.setStatus(0);introwsorderMapper.insertOrder(order);System.out.println(影响行数rows);System.out.println(回填后的主键order.getId());// 输出自增 ID}}八、总结主键回填是 MyBatis 中非常实用且高频使用的特性它解决了插入数据后立即获取主键的痛点。掌握两种实现方式useGeneratedKeys简洁高效MySQL 首选。selectKey灵活强大适配各种数据库及非自增主键场景。在实际开发中根据数据库类型和业务需求选择合适的方式可以大大简化代码逻辑提升开发效率。参考文档MyBatis 官方文档 - 获取自增主键MySQL LAST_INSERT_ID() 文档