@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 配合使用。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
statement | String | 是 | 获取主键的 SQL 语句 |
keyProperty | String | 是 | 主键回填到实体对象的哪个属性 |
keyColumn | String | 否 | 数据库主键列名,复合主键或列名不一致时使用 |
before | boolean | 否 | true 表示插入前获取主键(如 Oracle 序列),false 表示插入后获取(如 MySQL LAST_INSERT_ID)。默认 false |
resultType | Class<?> | 是 | 主键的类型,如 Integer.class、String.class、Long.class |
statementType | StatementType | 否 | 语句类型:STATEMENT、PREPARED、CALLABLE。默认 PREPARED |
核心原理
MyBatis 将 @SelectKey 解析为一个独立的 MappedStatement,标记为 SELECT 类型。执行时,根据 before 属性决定时机:
before = true:先执行@SelectKey的 SQL 获取主键,回填到实体对象,然后再执行@Insert的插入 SQLbefore = false:先执行@Insert的插入 SQL,然后执行@SelectKey的 SQL 获取生成的主键,再回填到实体对象
完整示例
场景说明
乐途公司学生管理系统需要演示两种 @SelectKey 用法:
- 插入前获取主键(模拟 Oracle 序列场景):使用
before = true,先查询序列值再插入 - 插入后获取主键(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);
当前数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 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
插入后数据状态
| id | name | age | major | score |
|---|---|---|---|---|
| 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 = true | MySQL 自增应 before = false 配合 LAST_INSERT_ID(),或直接用 @Options |
| resultType 与实际主键类型不匹配 | resultType = String.class 但主键是 INT | resultType 必须与数据库主键类型和实体属性类型一致 |
| 忘记给 keyProperty 指定属性 | keyProperty = "" | 必须指定实体类中接收主键的属性名,如 id |
| 认为 @SelectKey 比 @Options 更高级 | 所有场景都用 @SelectKey | MySQL 自增优先用 @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 的用法。