0%

mybatis之KeyGenerator生成主键

MyBatis 主键生成机制全解析:从原生 JDBC 到 KeyGenerator 适配

在数据库插入操作中,主键回填(获取插入后自动生成的主键)是高频需求(如 MySQL 自增 ID、Oracle 序列等)。MyBatis 基于原生 JDBC 主键回填逻辑,封装了 KeyGenerator 接口及其实现类,统一适配不同数据库的主键生成方式。本文从 “原生 JDBC 原理→MyBatis KeyGenerator 接口→三大实现类源码→实战配置” 逐步展开,彻底讲清 MyBatis 主键生成的底层逻辑与使用方法。

前置知识:原生 JDBC 主键回填

MyBatis 的主键生成机制源于 JDBC 原生支持,理解这一基础能更清晰地把握 MyBatis 的封装逻辑。

核心原理

JDBC 通过 PreparedStatementRETURN_GENERATED_KEYS 标识,告知数据库 “需要返回插入生成的主键”,插入后通过 getGeneratedKeys() 获取主键结果集。

原生代码示例(MySQL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 建立数据库连接
Connection connection = DriverManager.getConnection(url, username, password);

// 2. 创建 PreparedStatement,指定 RETURN_GENERATED_KEYS 标识
String sql = "INSERT INTO user (user_name, age) VALUES (?, ?)";
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

// 3. 绑定参数并执行插入
ps.setString(1, "张三");
ps.setInt(2, 20);
ps.executeUpdate(); // 执行插入,返回受影响行数

// 4. 获取生成的主键(结果集仅含主键列)
ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
long userId = rs.getLong(1); // 主键列索引从 1 开始
System.out.println("插入的用户ID:" + userId); // 输出如 "插入的用户ID:1001"
}

// 5. 关闭资源(省略)

原生方案的局限

  • 数据库兼容性差: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 {
/**
* 插入语句执行前调用(如:先获取 Oracle 序列值作为主键)
* @param executor 执行器
* @param ms MappedStatement(SQL 元信息)
* @param stmt Statement 对象
* @param parameter 插入参数对象(如 User)
*/
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

/**
* 插入语句执行后调用(如:获取 MySQL 自增主键)
* @param 参数同 processBefore
*/
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 方法(插入后获取自增主键),需配合 useGeneratedKeyskeyProperty 配置使用。

核心配置(Mapper.xml)
1
2
3
4
5
6
7
8
<!-- 
useGeneratedKeys="true":启用 JDBC 原生主键回填
keyProperty="id":指定主键对应参数对象的属性(如 User 的 id 字段)
keyColumn="id":可选,指定数据库主键列名(与表结构一致,默认与 keyProperty 同名)
-->
<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) {
// 1. 获取配置的主键属性(如 "id")
String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return; // 未配置主键属性,直接返回
}

try {
// 2. 调用 JDBC 原生方法获取主键结果集
ResultSet rs = stmt.getGeneratedKeys();
Throwable var6 = null;
try {
ResultSetMetaData rsmd = rs.getMetaData(); // 主键结果集元信息
// 3. 校验:数据库返回的主键列数 ≥ 配置的 keyProperties 数量
if (rsmd.getColumnCount() >= keyProperties.length) {
// 4. 将主键分配到参数对象(核心步骤)
assignKeys(ms.getConfiguration(), rs, rsmd, keyProperties, parameter);
}
} finally {
// 5. 关闭结果集,避免资源泄漏
if (rs != null) {
rs.close();
}
}
} catch (Exception e) {
throw new ExecutorException("获取主键失败:" + e.getMessage(), e);
}
}
}
关键子步骤:assignKeys 分配主键

assignKeys 负责将主键结果集中的主键值,通过反射(MetaObject) 设置回参数对象(如 Userid 属性):

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 {
// 处理不同参数类型(单个对象、List、Map)
if (parameter instanceof List) {
// 批量插入:为每个对象分配主键
assignKeysToParam(config, rs, rsmd, keyProperties, parameter);
} else if (parameter instanceof Map) {
// 参数为 Map:按 keyProperty 分配
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;
}

// 1. 创建主键分配器(按 keyProperties 配置,每个属性对应一个分配器)
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
assignerList.add(new KeyAssigner(config, rsmd, i + 1, null, keyProperties[i]));
}

// 2. 遍历主键结果集,为每个参数对象分配主键
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
// 异常:主键数量 > 参数对象数量(配置错误或驱动 bug)
throw new ExecutorException("主键数量超出参数对象数量,检查 keyProperty 配置");
}
Object param = iterator.next();
// 3. 调用分配器,将主键设置到参数对象
assignerList.forEach(assigner -> assigner.assign(rs, param));
}
}
内部类 KeyAssigner:反射设置主键

KeyAssignerJdbc3KeyGenerator 的内部类,负责通过 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; // 参数对象的主键属性名(如 "id")
private TypeHandler<?> typeHandler; // 类型处理器(处理 Java 与 JDBC 类型转换)

@Override
protected void assign(ResultSet rs, Object param) {
// 1. 创建参数对象的 MetaObject(方便反射操作)
MetaObject metaParam = configuration.newMetaObject(param);
try {
// 2. 初始化类型处理器(如 LongTypeHandler)
if (typeHandler == null) {
// 校验参数对象是否有主键属性的 setter 方法(无则抛异常)
if (!metaParam.hasSetter(propertyName)) {
throw new ExecutorException("参数对象无 " + propertyName + " 的 setter 方法");
}
// 获取主键属性的 Java 类型(如 Long)
Class<?> propertyType = metaParam.getSetterType(propertyName);
// 根据 Java 类型和 JDBC 类型获取 TypeHandler
typeHandler = configuration.getTypeHandlerRegistry()
.getTypeHandler(propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition)));
}

// 3. 通过 TypeHandler 从结果集获取主键值(JDBC→Java 类型转换)
Object keyValue = typeHandler.getResult(rs, columnPosition);
// 4. 反射设置主键到参数对象(如 user.setId(1001))
metaParam.setValue(propertyName, keyValue);
} catch (SQLException e) {
throw new ExecutorException("设置主键失败:" + e.getMessage(), e);
}
}
}
批量插入支持

Jdbc3KeyGenerator 天然支持批量插入的主键回填,只需保证参数为 List 且配置正确:

1
2
3
4
5
6
7
<!-- 批量插入用户,主键自动回填到每个 User 对象的 id 属性 -->
<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>:定义主键生成 SQL
keyProperty="id":主键对应参数对象的属性
resultType="long":主键类型
order="BEFORE":SQL 执行时机(BEFORE=插入前执行,AFTER=插入后执行)
-->
<selectKey keyProperty="id" resultType="long" order="BEFORE">
<!-- Oracle 序列:获取下一个序列值作为主键 -->
SELECT seq_user.nextval FROM dual
</selectKey>
<!-- 插入 SQL 中直接使用 #{id}(已由 <selectKey> 生成) -->
INSERT INTO user (id, user_name, age) VALUES (#{id}, #{userName}, #{age})
</insert>
源码解析:执行时机与逻辑

SelectKeyGenerator 的核心是根据 order 属性决定在 processBeforeprocessAfter 执行主键生成 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 {
// 执行时机:true=插入前执行(BEFORE),false=插入后执行(AFTER)
private final boolean executeBefore;
// <selectKey> 节点对应的 MappedStatement(封装主键生成 SQL)
private final MappedStatement keyStatement;

// 构造函数:初始化执行时机和 keyStatement
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
this.executeBefore = executeBefore;
this.keyStatement = keyStatement;
}

// 插入前执行(order="BEFORE")
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}

// 插入后执行(order="AFTER")
@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;
}

// 1. 获取 <selectKey> 配置的主键属性(如 "id")
String[] keyProperties = keyStatement.getKeyProperties();
Configuration config = ms.getConfiguration();
// 2. 创建参数对象的 MetaObject(反射工具)
MetaObject metaParam = config.newMetaObject(parameter);

// 3. 创建新的执行器,执行 <selectKey> 中的 SQL
Executor keyExecutor = config.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
// 4. 执行 SQL,获取主键结果(如 Oracle 序列值)
List<Object> keyValues = keyExecutor.query(
keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER
);

// 5. 校验主键结果(必须有且仅有一个结果)
if (keyValues.size() == 0) {
throw new ExecutorException("<selectKey> 未返回主键值");
}
if (keyValues.size() > 1) {
throw new ExecutorException("<selectKey> 返回多个主键值");
}

// 6. 解析主键结果(支持单个主键/联合主键)
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 {
// 联合主键:按 keyProperties 和 keyColumns 匹配设置
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>
<!-- 全局启用 JDBC 主键回填(默认 false) -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 全局指定主键属性名(默认无,需与实体类主键属性一致,如 "id") -->
<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. 注意事项

  1. keyProperty 必须有 setter 方法:MyBatis 通过反射设置主键,若无 setter 会抛 ExecutorException
  2. 批量插入需确保参数为 ListJdbc3KeyGenerator 仅对 List 类型参数支持批量主键回填;
  3. Oracle 序列需权限:执行 seq.nextval 的数据库用户需有序列的 SELECT 权限;
  4. keyColumn 与表结构一致:若数据库主键列名与 keyProperty 不同(如实体 userId vs 表 user_id),需显式配置 keyColumn="user_id"

总结

MyBatis 的主键生成机制通过 KeyGenerator 接口实现了数据库无关性,核心优势在于:

  1. 统一接口processBefore/processAfter 适配不同主键生成时机;
  2. 自动化封装:无需手动编写 JDBC 主键回填逻辑,配置即可用;
  3. 兼容性强:覆盖自增、序列、UUID 等所有主流主键生成方式

欢迎关注我的其它发布渠道