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

    • 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执行器类型
    • 分页插件

@SelectKey

导学

本节学习目标:

  • 掌握 @SelectKey 注解的核心属性,能够在插入前后获取主键值
  • 理解 before = true 和 before = false 的适用场景(Oracle 序列 vs MySQL LAST_INSERT_ID)
  • 学会配合 @Insert 使用 @SelectKey 实现主键回填
  • 明确 @SelectKey 与 @Options(useGeneratedKeys) 的适用边界

定义

@SelectKey 是 MyBatis 提供的主键获取注解,标注在 Mapper 接口的方法上,用于在 SQL 执行前或执行后通过额外的查询语句获取主键值,并回填到实体对象的指定属性中。它对应 XML 映射文件中的 <selectKey> 元素。

痛点解决:并非所有数据库都支持 JDBC 的 getGeneratedKeys()(如 Oracle 使用序列、某些分布式 ID 场景需要预生成 UUID)。@SelectKey 通过执行自定义 SQL 获取主键,填补了 @Options(useGeneratedKeys) 无法覆盖的场景,是跨数据库主键策略的通用解决方案。

注解方式 vs XML 方式对比

对比维度@SelectKey 注解方式XML <selectKey> 方式
声明位置写在 @Insert 方法上方写在 <insert> 内部
执行时机通过 before 属性控制通过 order 属性控制(BEFORE / AFTER)
功能完整性完全等价完全等价
可读性注解堆叠在方法上方XML 层级结构清晰

适用场景建议:Oracle 序列、预生成 UUID、自定义主键规则等场景必须使用 @SelectKey;MySQL 自增主键优先使用更简洁的 @Options(useGeneratedKeys = true)。

适用位置与核心属性

@SelectKey 标注在 Mapper 接口的方法 上,通常与 @Insert 配合使用。

属性类型必填说明
statementString是获取主键的 SQL 语句
keyPropertyString是主键回填到实体对象的哪个属性
keyColumnString否数据库主键列名,复合主键或列名不一致时使用
beforeboolean否true 表示插入前获取主键(如 Oracle 序列),false 表示插入后获取(如 MySQL LAST_INSERT_ID)。默认 false
resultTypeClass<?>是主键的类型,如 Integer.class、String.class、Long.class
statementTypeStatementType否语句类型:STATEMENT、PREPARED、CALLABLE。默认 PREPARED

核心原理

MyBatis 将 @SelectKey 解析为一个独立的 MappedStatement,标记为 SELECT 类型。执行时,根据 before 属性决定时机:

  • before = true:先执行 @SelectKey 的 SQL 获取主键,回填到实体对象,然后再执行 @Insert 的插入 SQL
  • before = false:先执行 @Insert 的插入 SQL,然后执行 @SelectKey 的 SQL 获取生成的主键,再回填到实体对象

完整示例

场景说明

乐途公司学生管理系统需要演示两种 @SelectKey 用法:

  1. 插入前获取主键(模拟 Oracle 序列场景):使用 before = true,先查询序列值再插入
  2. 插入后获取主键(MySQL LAST_INSERT_ID 场景):使用 before = false,插入后获取自增 ID

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

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

-- 模拟 Oracle 序列:创建一张序列表
CREATE TABLE id_sequence (
    seq_name VARCHAR(20) PRIMARY KEY,
    current_value INT
);
INSERT INTO id_sequence (seq_name, current_value) VALUES ('student_seq', 100);

-- 初始学生数据
INSERT INTO student (id, name, age, major, score) VALUES
(1, '大翔', 22, '计算机科学', 95.5),
(2, '白歌', 21, '软件工程', 88.0),
(3, '小崔', 20, '计算机科学', 92.0),
(4, '黄俪', 21, '信息安全', 90.5),
(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

完整的注解接口代码

实体类

package com.flying.entity;

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

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    public Double getScore() { return score; }
    public void setScore(Double score) { this.score = score; }
}

Mapper 接口

package com.flying.mapper;

import com.flying.entity.Student;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.SelectKey;

public interface StudentMapper {

    /**
     * 模拟 Oracle 序列场景:插入前先获取序列值作为主键
     * before = true,先执行 SELECT 获取 id,再执行 INSERT
     */
    @SelectKey(statement = "SELECT current_value + 1 FROM id_sequence WHERE seq_name = 'student_seq'",
               keyProperty = "id", keyColumn = "id", before = true, resultType = Integer.class)
    @Insert("INSERT INTO student(id, name, age, major, score) VALUES(#{id}, #{name}, #{age}, #{major}, #{score})")
    int insertWithSequence(Student student);

    /**
     * MySQL 场景:插入后获取 LAST_INSERT_ID
     * before = false,先执行 INSERT,再执行 SELECT LAST_INSERT_ID()
     * 注意:此示例中 student 表 id 为 AUTO_INCREMENT 时才有效,此处仅为演示语法
     */
    @SelectKey(statement = "SELECT LAST_INSERT_ID()",
               keyProperty = "id", keyColumn = "id", before = false, resultType = Integer.class)
    @Insert("INSERT INTO student(name, age, major, score) VALUES(#{name}, #{age}, #{major}, #{score})")
    int insertWithLastInsertId(Student student);
}

测试调用代码

package com.flying.test;

import com.flying.entity.Student;
import com.flying.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 SelectKeyTest {
    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);

        // 1. 模拟 Oracle 序列:插入前获取主键
        System.out.println("=== 插入前获取主键(模拟 Oracle 序列)===");
        Student stu1 = new Student();
        stu1.setName("赵新");
        stu1.setAge(23);
        stu1.setMajor("人工智能");
        stu1.setScore(91.0);

        int rows1 = mapper.insertWithSequence(stu1);
        System.out.println("影响行数: " + rows1);
        System.out.println("回填主键 id: " + stu1.getId());

        // 2. MySQL LAST_INSERT_ID:插入后获取主键
        System.out.println("\n=== 插入后获取主键(MySQL LAST_INSERT_ID)===");
        Student stu2 = new Student();
        stu2.setName("钱多多");
        stu2.setAge(20);
        stu2.setMajor("网络工程");
        stu2.setScore(85.0);

        int rows2 = mapper.insertWithLastInsertId(stu2);
        System.out.println("影响行数: " + rows2);
        System.out.println("回填主键 id: " + stu2.getId());

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

实际执行结果

控制台 SQL 输出

=== 插入前获取主键(模拟 Oracle 序列)===
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence!selectKey - ==>  Preparing: SELECT current_value + 1 FROM id_sequence WHERE seq_name = 'student_seq'
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence!selectKey - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence!selectKey - <==      Total: 1
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence - ==>  Preparing: INSERT INTO student(id, name, age, major, score) VALUES(?, ?, ?, ?, ?)
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence - ==> Parameters: 101(Integer), 赵新(String), 23(Integer), 人工智能(String), 91.0(Double)
[main] DEBUG com.flying.mapper.StudentMapper.insertWithSequence - <==    Updates: 1
影响行数: 1
回填主键 id: 101

=== 插入后获取主键(MySQL LAST_INSERT_ID)===
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId - ==>  Preparing: INSERT INTO student(name, age, major, score) VALUES(?, ?, ?, ?)
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId - ==> Parameters: 钱多多(String), 20(Integer), 网络工程(String), 85.0(Double)
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId - <==    Updates: 1
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId!selectKey - ==>  Preparing: SELECT LAST_INSERT_ID()
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId!selectKey - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.insertWithLastInsertId!selectKey - <==      Total: 1
影响行数: 1
回填主键 id: 6

插入后数据状态

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00
101赵新23人工智能91.00
6钱多多20网络工程85.00

分析

  • before = true 时,MyBatis 先执行 SELECT current_value + 1 获取序列值(101),回填到 stu1.id,然后执行 INSERT 时使用该值作为主键
  • before = false 时,MyBatis 先执行 INSERT,数据库自动生成主键(6),然后执行 SELECT LAST_INSERT_ID() 获取该值并回填
  • resultType = Integer.class 明确声明了主键类型,帮助 MyBatis 正确转换数据库返回值
  • 实际生产环境中,Oracle 序列通常还会配合 UPDATE id_sequence SET current_value = current_value + 1 来递增序列值

易错场景 / 常见误区

误区错误示例正解
before 取值与数据库特性不匹配MySQL 自增用 before = trueMySQL 自增应 before = false 配合 LAST_INSERT_ID(),或直接用 @Options
resultType 与实际主键类型不匹配resultType = String.class 但主键是 INTresultType 必须与数据库主键类型和实体属性类型一致
忘记给 keyProperty 指定属性keyProperty = ""必须指定实体类中接收主键的属性名,如 id
认为 @SelectKey 比 @Options 更高级所有场景都用 @SelectKeyMySQL 自增优先用 @Options(useGeneratedKeys = true),更简洁高效

面试考点

Q1:@SelectKey 的 before = true 和 before = false 分别适用于什么数据库?

before = true 适用于插入前需要预生成主键的场景,如 Oracle 序列、自定义 UUID 生成规则;before = false 适用于插入后获取数据库生成的主键,如 MySQL 的 LAST_INSERT_ID()、SQL Server 的 SCOPE_IDENTITY()。

Q2:@SelectKey 和 @Options(useGeneratedKeys) 有什么区别?

@Options(useGeneratedKeys = true) 依赖 JDBC 驱动的 getGeneratedKeys() 能力,由 JDBC 驱动直接返回生成键,效率更高,但仅适用于支持自动生成键的数据库;@SelectKey 通过执行额外 SQL 获取主键,通用性更强,适用于所有数据库,但多一次数据库往返。

Q3:statementType 属性一般什么时候需要修改?

默认 PREPARED 适用于绝大多数场景。当 statement 是存储过程调用时,需改为 CALLABLE;当需要直接执行静态 SQL 而不预编译时,可改为 STATEMENT(不推荐,存在 SQL 注入风险)。

Q4:@SelectKey 可以配合 @Update 使用吗?

可以。虽然最常见于 @Insert,但 @SelectKey 也可用于 @Update。例如更新后需要获取某个计算列的最新值,或更新触发器生成的新值。

小结

@SelectKey 是跨数据库主键策略的通用解决方案。before = true 用于预生成主键(Oracle 序列),before = false 用于后获取主键(MySQL LAST_INSERT_ID)。在 MySQL 自增场景下,优先使用更简洁的 @Options(useGeneratedKeys = true);在 Oracle 或自定义主键规则场景下,@SelectKey 是不可或缺的工具。

下一章引子

静态 SQL 无法应对复杂多变的查询条件。@SelectProvider 注解通过指定 Provider 类动态生成 SQL,是注解方式下实现动态 SQL 的核心方案。下一节将详细讲解 @SelectProvider 的用法。

上一页
@Options
下一页
@Results