乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

主键生成策略

导学

本节将掌握MyBatis中两种主键获取策略的配置与使用。你将理解useGeneratedKeys与selectKey的适用数据库场景,掌握selectKey的完整属性配置,能够针对MySQL自增主键和模拟Oracle序列分别写出正确的映射文件,并理解二者在时序上的本质差异。

定义

useGeneratedKeys与selectKey是MyBatis中用于获取数据库生成主键的两种机制。在JDBC原始写法中,获取插入后的主键需要手动调用Statement.getGeneratedKeys(),遍历返回的ResultSet提取主键值,代码冗长且数据库兼容性差。MyBatis将这一过程标准化:useGeneratedKeys利用JDBC标准接口直接获取自增主键,selectKey则通过执行额外的查询语句获取主键值。二者分别适用于不同的数据库主键生成策略,共同解决了"插入后如何知道新记录ID"这一持久层核心问题。

适用位置与核心属性

useGeneratedKeys(insert元素的属性)

属性是否必填说明
useGeneratedKeys否是否使用JDBC的getGeneratedKeys获取主键,默认false
keyProperty否将主键值回填到实体对象的哪个属性
keyColumn否当主键列名与keyProperty不一致时指定数据库列名

selectKey(insert元素内部的子元素)

属性是否必填说明
keyProperty否将查询结果回填到实体对象的哪个属性
keyColumn否数据库返回结果集中主键所在的列名
resultType否查询结果的Java类型
order否BEFORE(插入前获取)或AFTER(插入后获取)
statementType否STATEMENT、PREPARED或CALLABLE,默认PREPARED

核心原理

两种主键获取策略在时序和实现机制上存在本质差异。

  1. useGeneratedKeys:依赖JDBC驱动的getGeneratedKeys()方法,在INSERT执行后立即获取数据库生成的主键。这是最高效的方式,但要求数据库和驱动支持该特性(MySQL完全支持)。
  2. selectKey BEFORE:在INSERT之前执行查询(如Oracle序列的NEXTVAL),获取主键后先回填到实体对象,再执行带主键的INSERT。适用于主键由外部序列生成且插入时必须指定主键的场景。
  3. selectKey AFTER:在INSERT之后执行查询(如MySQL的LAST_INSERT_ID()),获取主键后回填到实体对象。适用于数据库不支持getGeneratedKeys()但支持查询获取最后生成ID的场景。

完整示例

场景说明

乐途公司技术部使用MySQL 5.7作为主数据库,主键采用自增策略。同时,系统需要兼容Oracle部署环境,Oracle使用序列生成主键。本节演示两种环境下的主键获取配置:MySQL使用useGeneratedKeys,模拟Oracle环境使用selectKey。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整的映射文件片段与Java代码

POJO类

package com.flywing.entity;

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String major;
    private Double score;

    // Getter与Setter省略
}

Mapper接口

package com.flywing.mapper;

import com.flywing.entity.Student;

public interface StudentMapper {
    // MySQL自增主键:useGeneratedKeys
    int insertMySQL(Student student);

    // 模拟Oracle序列:selectKey BEFORE
    int insertOracle(Student student);

    // selectKey AFTER:兼容旧版驱动
    int insertWithSelectKeyAfter(Student student);
}

映射文件 StudentMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.flywing.mapper.StudentMapper">

    <!--
        场景一:MySQL自增主键
        useGeneratedKeys 启用JDBC getGeneratedKeys
        keyProperty 指定回填到Student的id属性
    -->
    <insert id="insertMySQL" parameterType="com.flywing.entity.Student"
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO student (name, age, major, score)
        VALUES (#{name}, #{age}, #{major}, #{score})
    </insert>

    <!--
        场景二:模拟Oracle序列(BEFORE)
        在插入前查询序列值,回填到id属性,然后执行带id的INSERT
        实际Oracle中:SELECT seq_student.NEXTVAL FROM dual
        此处用MySQL变量模拟序列行为
    -->
    <insert id="insertOracle" parameterType="com.flywing.entity.Student">
        <selectKey keyProperty="id" resultType="int" order="BEFORE">
            SELECT IFNULL(MAX(id), 0) + 1 FROM student
        </selectKey>
        INSERT INTO student (id, name, age, major, score)
        VALUES (#{id}, #{name}, #{age}, #{major}, #{score})
    </insert>

    <!--
        场景三:selectKey AFTER(MySQL LAST_INSERT_ID)
        在插入后查询最后生成的自增ID
        适用于不支持getGeneratedKeys的旧驱动
    -->
    <insert id="insertWithSelectKeyAfter" parameterType="com.flywing.entity.Student">
        <selectKey keyProperty="id" resultType="int" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
        INSERT INTO student (name, age, major, score)
        VALUES (#{name}, #{age}, #{major}, #{score})
    </insert>

</mapper>

测试代码

package com.flywing.test;

import com.flywing.entity.Student;
import com.flywing.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class KeyGeneratorTest {
    public static void main(String[] args) throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        SqlSession session = factory.openSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);

        // 场景一:MySQL useGeneratedKeys
        Student s1 = new Student();
        s1.setName("MySQL新生");
        s1.setAge(19);
        s1.setMajor("数据科学");
        s1.setScore(91.0);
        System.out.println("插入前ID:" + s1.getId()); // null
        mapper.insertMySQL(s1);
        System.out.println("useGeneratedKeys 插入后ID:" + s1.getId());

        // 场景二:模拟Oracle序列 BEFORE
        Student s2 = new Student();
        s2.setName("Oracle新生");
        s2.setAge(20);
        s2.setMajor("网络安全");
        s2.setScore(89.5);
        System.out.println("\n插入前ID:" + s2.getId()); // null
        mapper.insertOracle(s2);
        System.out.println("selectKey BEFORE 插入后ID:" + s2.getId());

        // 场景三:selectKey AFTER
        Student s3 = new Student();
        s3.setName("兼容新生");
        s3.setAge(21);
        s3.setMajor("云计算");
        s3.setScore(93.0);
        System.out.println("\n插入前ID:" + s3.getId()); // null
        mapper.insertWithSelectKeyAfter(s3);
        System.out.println("selectKey AFTER 插入后ID:" + s3.getId());

        session.commit();
        session.close();
    }
}

实际执行结果

控制台SQL输出

插入前ID:null
[DEBUG] com.flywing.mapper.StudentMapper.insertMySQL - ==>  Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[DEBUG] com.flywing.mapper.StudentMapper.insertMySQL - ==> Parameters: MySQL新生(String), 19(Integer), 数据科学(String), 91.0(Double)
[DEBUG] com.flywing.mapper.StudentMapper.insertMySQL - <==    Updates: 1
useGeneratedKeys 插入后ID:6

插入前ID:null
[DEBUG] com.flywing.mapper.StudentMapper.insertOracle - ==>  Preparing: SELECT IFNULL(MAX(id), 0) + 1 FROM student
[DEBUG] com.flywing.mapper.StudentMapper.insertOracle - <==      Total: 1
[DEBUG] com.flywing.mapper.StudentMapper.insertOracle - ==>  Preparing: INSERT INTO student (id, name, age, major, score) VALUES (?, ?, ?, ?, ?)
[DEBUG] com.flywing.mapper.StudentMapper.insertOracle - ==> Parameters: 7(Integer), Oracle新生(String), 20(Integer), 网络安全(String), 89.5(Double)
[DEBUG] com.flywing.mapper.StudentMapper.insertOracle - <==    Updates: 1
selectKey BEFORE 插入后ID:7

插入前ID:null
[DEBUG] com.flywing.mapper.StudentMapper.insertWithSelectKeyAfter - ==>  Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
[DEBUG] com.flywing.mapper.StudentMapper.insertWithSelectKeyAfter - ==> Parameters: 兼容新生(String), 21(Integer), 云计算(String), 93.0(Double)
[DEBUG] com.flywing.mapper.StudentMapper.insertWithSelectKeyAfter - <==    Updates: 1
[DEBUG] com.flywing.mapper.StudentMapper.insertWithSelectKeyAfter - ==>  Preparing: SELECT LAST_INSERT_ID()
[DEBUG] com.flywing.mapper.StudentMapper.insertWithSelectKeyAfter - <==      Total: 1
selectKey AFTER 插入后ID:8

插入后的数据库状态

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00
6MySQL新生19数据科学91.00
7Oracle新生20网络安全89.50
8兼容新生21云计算93.00

分析

  1. useGeneratedKeys的效率优势:insertMySQL只与数据库交互一次,INSERT完成后驱动自动带回主键。这是MySQL环境下最推荐的方式,简洁且性能最优。
  2. selectKey BEFORE的序列模拟:insertOracle在插入前执行SELECT IFNULL(MAX(id), 0) + 1,模拟Oracle序列的NEXTVAL行为。查询到的新ID先回填到实体对象,随后INSERT语句显式携带该ID。这种方式适用于主键不由数据库自动生成、而由外部序列或业务规则生成的场景。
  3. selectKey AFTER的兼容性:insertWithSelectKeyAfter在插入后执行SELECT LAST_INSERT_ID(),这是MySQL特有的函数。该方式比useGeneratedKeys多一次数据库往返,但在旧版JDBC驱动不支持getGeneratedKeys()时是可靠的降级方案。

易错场景/常见误区

误区正解
MySQL环境用selectKey代替useGeneratedKeysMySQL优先使用useGeneratedKeys,只需一次交互;selectKey需要额外查询,性能较差
selectKey的order写错Oracle序列用BEFORE(先取主键再插入),MySQLLAST_INSERT_ID用AFTER(先插入再取主键)
selectKey的resultType与实体属性类型不一致如resultType="int"但实体属性是Long,会导致类型转换异常
批量插入时期望useGeneratedKeys回填所有对象的主键部分JDBC驱动在批量插入时只能返回第一个生成的主键;批量场景建议逐条插入或使用selectKey
keyProperty写数据库列名keyProperty始终写Java实体属性名;数据库列名用keyColumn指定

面试考点

Q1:useGeneratedKeys和selectKey有什么区别?

useGeneratedKeys依赖JDBC驱动的getGeneratedKeys()接口,在INSERT执行后自动获取数据库生成的主键,仅适用于支持该特性的数据库(如MySQL)。selectKey通过执行额外的SQL查询获取主键值,支持BEFORE(插入前获取,如Oracle序列)和AFTER(插入后获取,如LAST_INSERT_ID)两种时序,兼容性更强但多一次数据库交互。

Q2:Oracle数据库应该如何配置主键获取?

Oracle没有自增列,通常使用序列生成主键。应在insert中配置<selectKey keyProperty="id" resultType="int" order="BEFORE">SELECT seq_name.NEXTVAL FROM dual</selectKey>,在插入前查询序列值并回填到实体对象,然后执行带主键的INSERT。

Q3:为什么selectKey的order属性很重要?

order决定selectKey与INSERT的执行顺序。BEFORE适用于需要在插入前就知道主键值的场景(如Oracle序列、UUID生成);AFTER适用于插入后才能确定主键值的场景(如MySQL自增、触发器生成主键)。写错顺序会导致插入时主键为null或查询不到主键。

Q4:批量插入时如何获取每条记录的主键?

MyBatis的useGeneratedKeys在批量插入时,部分JDBC驱动只能回填第一个对象的主键。若需获取所有主键,可改用逐条插入(事务内循环),或使用数据库特定的批量插入语法配合selectKey。MySQL的INSERT ... VALUES (), (), ()配合useGeneratedKeys在较新驱动中已支持返回多条生成主键。

小结

useGeneratedKeys与selectKey是MyBatis应对不同数据库主键生成策略的两把钥匙。MySQL自增主键优先使用useGeneratedKeys,一次交互、简洁高效;Oracle序列或特殊主键生成规则使用selectKey,通过BEFORE或AFTER控制时序,灵活兼容。理解二者的执行时序差异和适用边界,是设计可移植持久层代码的基础。

下一章引子

映射文件中的SQL语句往往存在大量重复片段:多个查询都需要相同的字段列表,多个条件都需要相同的WHERE子句。复制粘贴不仅冗余,维护时也容易遗漏。sql片段元素允许将重复的SQL代码提取为可复用的模块,通过include标签在多处引用,甚至支持参数传递实现更灵活的复用。下一节将讲解sql片段的定义、引用和参数传递机制。

上一页
参数传递与占位符
下一页
resultType