mybatis之KeyGenerator生成主键
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. 建立数据库连接
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 主键生成的顶层接口,定义了主键生成的两个关键时机(插入前后),所有主键生成逻辑均通过该接口实现。
接口定义与核心方法
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 的默认主键生成器,无任何逻辑,适用于不需要获取插入后主键的场景(如仅关注插入是否成功)。
源码实现
public class NoKeyGenerator implements KeyGenerator {
// 单例实例(避免重复创建)
public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// 空实现:插入前不做任何操作
}
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)
<!--
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 是核心入口,负责在插入后获取主键并设置回参数对象:
public class Jdbc3KeyGenerator implements KeyGenerator {
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
// 插入后执行:获取自增主键
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) 设置回参数对象(如 User 的 id 属性):
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:反射设置主键
KeyAssigner 是 Jdbc3KeyGenerator 的内部类,负责通过 MetaObject(MyBatis 反射工具)将主键值设置到参数对象的属性:
private class KeyAssigner {
private final Configuration configuration;
private final int columnPosition; // 主键在结果集中的列索引
private final String propertyName; // 参数对象的主键属性名(如 "id")
private TypeHandler<?> typeHandler; // 类型处理器(处理 Java 与 JDBC 类型转换)
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 且配置正确:
<!-- 批量插入用户,主键自动回填到每个 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 序列示例)
<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 属性决定在 processBefore 或 processAfter 执行主键生成 SQL:
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")
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
// 插入后执行(order="AFTER")
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (!executeBefore) {
processGeneratedKeys(executor, ms, parameter);
}
}
}
核心逻辑:processGeneratedKeys 生成主键
processGeneratedKeys 负责执行 <selectKey> 中的 SQL,获取主键并设置回参数对象:
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,再插入。
- Oracle 序列(
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 自增),可通过全局配置简化局部配置:
<configuration>
<settings>
<!-- 全局启用 JDBC 主键回填(默认 false) -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 全局指定主键属性名(默认无,需与实体类主键属性一致,如 "id") -->
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
</configuration>
全局配置后,局部 Mapper 无需再写 useGeneratedKeys="true",只需指定 keyProperty:
<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不同(如实体userIdvs 表user_id),需显式配置keyColumn="user_id"。
总结
MyBatis 的主键生成机制通过 KeyGenerator 接口实现了数据库无关性,核心优势在于:
- 统一接口:
processBefore/processAfter适配不同主键生成时机; - 自动化封装:无需手动编写 JDBC 主键回填逻辑,配置即可用;
- 兼容性强:覆盖自增、序列、UUID 等所有主流主键生成方式