MyBatis 主键生成机制全解析:从原生 JDBC 到 KeyGenerator 适配 在数据库插入操作中,主键回填 (获取插入后自动生成的主键)是高频需求(如 MySQL 自增 ID、Oracle 序列等)。MyBatis 基于原生 JDBC 主键回填逻辑,封装了 KeyGenerator 接口及其实现类,统一适配不同数据库的主键生成方式。本文从 “原生 JDBC 原理→MyBatis KeyGenerator 接口→三大实现类源码→实战配置” 逐步展开,彻底讲清 MyBatis 主键生成的底层逻辑与使用方法。
前置知识:原生 JDBC 主键回填 MyBatis 的主键生成机制源于 JDBC 原生支持,理解这一基础能更清晰地把握 MyBatis 的封装逻辑。
核心原理 JDBC 通过 PreparedStatement 的 RETURN_GENERATED_KEYS 标识 ,告知数据库 “需要返回插入生成的主键”,插入后通过 getGeneratedKeys() 获取主键结果集。
原生代码示例(MySQL) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Connection connection = DriverManager.getConnection(url, username, password);String sql = "INSERT INTO user (user_name, age) VALUES (?, ?)" ;PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);ps.setString(1 , "张三" ); ps.setInt(2 , 20 ); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys();if (rs.next()) { long userId = rs.getLong(1 ); System.out.println("插入的用户ID:" + userId); }
原生方案的局限
数据库兼容性差 :MySQL 支持自增 +RETURN_GENERATED_KEYS,但 Oracle 需通过序列(seq.nextval)生成主键,无法直接用此方案;
代码冗余 :每次插入都需重复 “指定标识→获取结果集” 逻辑;
批量插入复杂 :批量插入时需手动分配主键到每个对象。
MyBatis 的 KeyGenerator 正是为解决这些问题而生,通过统一接口适配不同数据库的主键生成逻辑。
MyBatis 主键生成核心:KeyGenerator 接口 KeyGenerator 是 MyBatis 主键生成的顶层接口 ,定义了主键生成的两个关键时机(插入前后),所有主键生成逻辑均通过该接口实现。
接口定义与核心方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface KeyGenerator { void processBefore (Executor executor, MappedStatement ms, Statement stmt, Object parameter) ; void processAfter (Executor executor, MappedStatement ms, Statement stmt, Object parameter) ; }
核心设计思路 MyBatis 通过 “执行时机(Before/After)+ 具体实现” 适配不同主键生成方式:
processBefore :适用于 “先生成主键,再插入” 的场景(如 Oracle 序列、UUID);
processAfter :适用于 “先插入,再获取主键” 的场景(如 MySQL 自增、PostgreSQL 序列)。
MyBatis 提供 3 个 KeyGenerator 实现类,覆盖所有主流场景:
实现类
适用场景
执行时机
核心逻辑
NoKeyGenerator
无需主键回填(默认)
无(空实现)
不处理主键
Jdbc3KeyGenerator
支持自增主键的数据库(MySQL、SQL Server)
processAfter
利用 JDBC getGeneratedKeys() 获取主键
SelectKeyGenerator
不支持自增的数据库(Oracle 序列)
可配置(Before/After)
执行 <selectKey> 节点的 SQL 生成主键
KeyGenerator 三大实现类源码解析 1. NoKeyGenerator:默认空实现 NoKeyGenerator 是 MyBatis 的默认主键生成器,无任何逻辑 ,适用于不需要获取插入后主键的场景(如仅关注插入是否成功)。
源码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class NoKeyGenerator implements KeyGenerator { public static final NoKeyGenerator INSTANCE = new NoKeyGenerator (); @Override public void processBefore (Executor executor, MappedStatement ms, Statement stmt, Object parameter) { } @Override public void processAfter (Executor executor, MappedStatement ms, Statement stmt, Object parameter) { } }
适用场景:
插入操作无需后续使用主键(如日志插入);
主键由业务层生成(如 UUID 手动设置到参数对象)。
2. Jdbc3KeyGenerator:数据库自增主键适配(MySQL/SQL Server) Jdbc3KeyGenerator 是 MyBatis 对 JDBC 原生主键回填的封装,仅实现 processAfter 方法 (插入后获取自增主键),需配合 useGeneratedKeys 和 keyProperty 配置使用。
核心配置(Mapper.xml) 1 2 3 4 5 6 7 8 <insert id ="insertUser" useGeneratedKeys ="true" keyProperty ="id" keyColumn ="id" > INSERT INTO user (user_name, age) VALUES (#{userName}, #{age}) </insert >
源码解析:processAfter 方法 processAfter 是核心入口,负责在插入后获取主键并设置回参数对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Jdbc3KeyGenerator implements KeyGenerator { public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator (); @Override public void processAfter (Executor executor, MappedStatement ms, Statement stmt, Object parameter) { processBatch(ms, stmt, parameter); } public void processBatch (MappedStatement ms, Statement stmt, Object parameter) { String[] keyProperties = ms.getKeyProperties(); if (keyProperties == null || keyProperties.length == 0 ) { return ; } try { ResultSet rs = stmt.getGeneratedKeys(); Throwable var6 = null ; try { ResultSetMetaData rsmd = rs.getMetaData(); if (rsmd.getColumnCount() >= keyProperties.length) { assignKeys(ms.getConfiguration(), rs, rsmd, keyProperties, parameter); } } finally { if (rs != null ) { rs.close(); } } } catch (Exception e) { throw new ExecutorException ("获取主键失败:" + e.getMessage(), e); } } }
关键子步骤:assignKeys 分配主键 assignKeys 负责将主键结果集中的主键值,通过反射(MetaObject) 设置回参数对象(如 User 的 id 属性):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 private void assignKeys (Configuration config, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, Object parameter) throws SQLException { if (parameter instanceof List) { assignKeysToParam(config, rs, rsmd, keyProperties, parameter); } else if (parameter instanceof Map) { assignKeysToParamMap(config, rs, rsmd, keyProperties, (Map) parameter); } else { assignKeysToParam(config, rs, rsmd, keyProperties, parameter); } } private void assignKeysToParam (Configuration config, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, Object parameter) throws SQLException { Collection<?> params = collectionize(parameter); if (params.isEmpty()) { return ; } List<KeyAssigner> assignerList = new ArrayList <>(); for (int i = 0 ; i < keyProperties.length; i++) { assignerList.add(new KeyAssigner (config, rsmd, i + 1 , null , keyProperties[i])); } Iterator<?> iterator = params.iterator(); while (rs.next()) { if (!iterator.hasNext()) { throw new ExecutorException ("主键数量超出参数对象数量,检查 keyProperty 配置" ); } Object param = iterator.next(); assignerList.forEach(assigner -> assigner.assign(rs, param)); } }
内部类 KeyAssigner:反射设置主键 KeyAssigner 是 Jdbc3KeyGenerator 的内部类,负责通过 MetaObject(MyBatis 反射工具)将主键值设置到参数对象的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private class KeyAssigner { private final Configuration configuration; private final int columnPosition; private final String propertyName; private TypeHandler<?> typeHandler; @Override protected void assign (ResultSet rs, Object param) { MetaObject metaParam = configuration.newMetaObject(param); try { if (typeHandler == null ) { if (!metaParam.hasSetter(propertyName)) { throw new ExecutorException ("参数对象无 " + propertyName + " 的 setter 方法" ); } Class<?> propertyType = metaParam.getSetterType(propertyName); typeHandler = configuration.getTypeHandlerRegistry() .getTypeHandler(propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition))); } Object keyValue = typeHandler.getResult(rs, columnPosition); metaParam.setValue(propertyName, keyValue); } catch (SQLException e) { throw new ExecutorException ("设置主键失败:" + e.getMessage(), e); } } }
批量插入支持 Jdbc3KeyGenerator 天然支持批量插入的主键回填,只需保证参数为 List 且配置正确:
1 2 3 4 5 6 7 <insert id ="batchInsertUser" useGeneratedKeys ="true" keyProperty ="id" > INSERT INTO user (user_name, age) VALUES <foreach item ="item" collection ="list" separator ="," > (#{item.userName}, #{item.age}) </foreach > </insert >
调用后,List<User> 中的每个 User 对象的 id 会被自动设置为数据库生成的自增主键。
3. SelectKeyGenerator:非自增主键适配(Oracle 序列) 对于不支持自增主键的数据库(如 Oracle),MyBatis 提供 SelectKeyGenerator,通过 <selectKey> 节点执行自定义 SQL 生成主键(如调用序列、UUID 函数)。
核心配置(Mapper.xml,Oracle 序列示例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <insert id ="insertUser" > <selectKey keyProperty ="id" resultType ="long" order ="BEFORE" > SELECT seq_user.nextval FROM dual </selectKey > INSERT INTO user (id, user_name, age) VALUES (#{id}, #{userName}, #{age}) </insert >
源码解析:执行时机与逻辑 SelectKeyGenerator 的核心是根据 order 属性决定在 processBefore 或 processAfter 执行主键生成 SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class SelectKeyGenerator implements KeyGenerator { private final boolean executeBefore; private final MappedStatement keyStatement; public SelectKeyGenerator (MappedStatement keyStatement, boolean executeBefore) { this .executeBefore = executeBefore; this .keyStatement = keyStatement; } @Override public void processBefore (Executor executor, MappedStatement ms, Statement stmt, Object parameter) { if (executeBefore) { processGeneratedKeys(executor, ms, parameter); } } @Override public void processAfter (Executor executor, MappedStatement ms, Statement stmt, Object parameter) { if (!executeBefore) { processGeneratedKeys(executor, ms, parameter); } } }
核心逻辑:processGeneratedKeys 生成主键 processGeneratedKeys 负责执行 <selectKey> 中的 SQL,获取主键并设置回参数对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 private void processGeneratedKeys (Executor executor, MappedStatement ms, Object parameter) { try { if (parameter == null || keyStatement == null ) { return ; } String[] keyProperties = keyStatement.getKeyProperties(); Configuration config = ms.getConfiguration(); MetaObject metaParam = config.newMetaObject(parameter); Executor keyExecutor = config.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE); List<Object> keyValues = keyExecutor.query( keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER ); if (keyValues.size() == 0 ) { throw new ExecutorException ("<selectKey> 未返回主键值" ); } if (keyValues.size() > 1 ) { throw new ExecutorException ("<selectKey> 返回多个主键值" ); } MetaObject metaResult = config.newMetaObject(keyValues.get(0 )); if (keyProperties.length == 1 ) { Object keyValue = metaResult.hasGetter(keyProperties[0 ]) ? metaResult.getValue(keyProperties[0 ]) : keyValues.get(0 ); setValue(metaParam, keyProperties[0 ], keyValue); } else { handleMultipleProperties(keyProperties, metaParam, metaResult); } } catch (Exception e) { throw new ExecutorException ("生成主键失败:" + e.getMessage(), e); } } private void setValue (MetaObject metaParam, String property, Object value) { if (metaParam.hasSetter(property)) { metaParam.setValue(property, value); } else { throw new ExecutorException ("参数对象无 " + property + " 的 setter 方法" ); } }
执行时机选择(order 属性)
order="BEFORE":主键生成 SQL 在插入前执行(推荐),适用于:
Oracle 序列(seq.nextval):先获取序列值作为主键,再插入;
UUID 函数(SELECT UUID() FROM dual):先生成 UUID,再插入。
order="AFTER":主键生成 SQL 在插入后执行,适用于:
数据库支持 “插入后查询主键” 但不支持自增的场景(如某些老版本 DB2);
主键由触发器生成(插入后通过查询获取触发器生成的主键)。
实战总结:不同场景的主键生成方案 1. 场景对比与配置
数据库 / 场景
推荐实现类
核心配置(Mapper.xml)
执行时机
MySQL 自增主键
Jdbc3KeyGenerator
<insert useGeneratedKeys="true" keyProperty="id">INSERT ...</insert>
插入后
SQL Server 自增主键
Jdbc3KeyGenerator
同 MySQL(需确保表主键为 IDENTITY 类型)
插入后
Oracle 序列
SelectKeyGenerator
<insert><selectKey keyProperty="id" order="BEFORE">SELECT seq.nextval FROM dual</selectKey>INSERT ...</insert>
插入前
通用 UUID 主键
SelectKeyGenerator
<insert><selectKey keyProperty="id" order="BEFORE">SELECT UUID()</selectKey>INSERT ...</insert>
插入前
无需主键回填
NoKeyGenerator(默认)
无需额外配置,直接写 <insert>INSERT ...</insert>
无
2. 全局配置(mybatis-config.xml) 若所有表均使用同一主键生成方式(如 MySQL 自增),可通过全局配置简化局部配置:
1 2 3 4 5 6 7 8 <configuration > <settings > <setting name ="useGeneratedKeys" value ="true" /> <setting name ="defaultExecutorType" value ="SIMPLE" /> </settings > </configuration >
全局配置后,局部 Mapper 无需再写 useGeneratedKeys="true",只需指定 keyProperty:
1 2 3 <insert id ="insertUser" keyProperty ="id" > INSERT INTO user (user_name, age) VALUES (#{userName}, #{age}) </insert >
3. 注意事项
keyProperty 必须有 setter 方法 :MyBatis 通过反射设置主键,若无 setter 会抛 ExecutorException;
批量插入需确保参数为 List :Jdbc3KeyGenerator 仅对 List 类型参数支持批量主键回填;
Oracle 序列需权限 :执行 seq.nextval 的数据库用户需有序列的 SELECT 权限;
keyColumn 与表结构一致 :若数据库主键列名与 keyProperty 不同(如实体 userId vs 表 user_id),需显式配置 keyColumn="user_id"。
总结 MyBatis 的主键生成机制通过 KeyGenerator 接口实现了数据库无关性 ,核心优势在于:
统一接口 :processBefore/processAfter 适配不同主键生成时机;
自动化封装 :无需手动编写 JDBC 主键回填逻辑,配置即可用;
兼容性强 :覆盖自增、序列、UUID 等所有主流主键生成方式