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. 建立数据库连接
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();

    @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)
<!-- 
  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();

    // 插入后执行:获取自增主键
    @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 属性):

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 反射工具)将主键值设置到参数对象的属性:

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 且配置正确:

<!-- 批量插入用户,主键自动回填到每个 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 属性决定在 processBeforeprocessAfter 执行主键生成 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")
    @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,获取主键并设置回参数对象:

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 自增),可通过全局配置简化局部配置:

<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. 注意事项

  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 等所有主流主键生成方式