@Select
导学
本节学习目标:
- 掌握
@Select注解的基本用法,能够替代 XML 中的<select>标签完成数据查询 - 理解注解方式与 XML 方式在查询场景下的各自优劣,能够根据团队规范选择合适方案
- 学会使用
@Select实现基本查询、条件查询、模糊查询、多表 JOIN 查询 - 掌握返回单个对象、返回
List、返回Map三种结果形式的写法
定义
@Select 是 MyBatis 提供的核心查询注解,直接标注在 Mapper 接口的方法上,用于声明该方法对应的 SQL 查询语句。它对应 XML 映射文件中的 <select> 元素。
痛点解决:在纯注解开发模式下,开发者无需维护独立的 XML 映射文件,SQL 与接口方法集中在一处,降低了文件切换成本,特别适合 SQL 较短、结构简单的查询场景。
注解方式 vs XML 方式对比
| 对比维度 | @Select 注解方式 | XML <select> 方式 |
|---|---|---|
| 代码位置 | SQL 直接写在接口方法上方 | SQL 写在独立的 XML 文件中 |
| 可读性 | 短 SQL 直观,长 SQL 可读性下降 | 长 SQL、复杂动态 SQL 可读性更好 |
| 动态 SQL | 需借助 <script> 标签或 Provider 类 | 原生支持 <if>、<where>、<foreach> 等标签 |
| 团队协作 | 适合小型项目或简单查询 | 适合大型项目,SQL 可由 DBA 集中维护 |
| 热部署 | 修改 SQL 需重新编译 | 修改 XML 无需重新编译(部分配置下) |
适用场景建议:单表查询、简单 JOIN、SQL 行数不超过 20 行的场景优先使用
@Select;涉及复杂动态条件、多表嵌套关联、需要频繁调整 SQL 的场景建议使用 XML。
适用位置与核心属性
@Select 只能标注在 Mapper 接口的方法 上。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
value | String | 是 | 要执行的 SQL 语句。支持 ${} 和 #{} 占位符。当只有一个属性时,可省略 value = 直接写字符串 |
核心原理
MyBatis 在启动阶段扫描 Mapper 接口时,如果发现方法上标注了 @Select,会将该注解的 value 值解析为 MappedStatement 对象中的 SQL 源码。后续通过动态代理生成 Mapper 接口的代理对象,当调用代理方法时,MyBatis 从 MappedStatement 中获取 SQL,经参数绑定后交由 JDBC 执行。
完整示例
场景说明
乐途公司学生管理系统需要实现以下查询功能:查看所有学生列表、根据主键查询单个学生、按专业模糊搜索学生、统计各专业人数。
操作前的数据库表结构及初始数据
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);
当前数据状态:
| 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;
// Getter 和 Setter 省略
@Override
public String toString() {
return "Student{id=" + id + ", name='" + name + "', age=" + age +
", major='" + major + "', score=" + score + "}";
}
}
Mapper 接口
package com.flying.mapper;
import com.flying.entity.Student;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
public interface StudentMapper {
/**
* 查询所有学生
*/
@Select("SELECT id, name, age, major, score FROM student")
List<Student> selectAll();
/**
* 根据 ID 查询单个学生
*/
@Select("SELECT id, name, age, major, score FROM student WHERE id = #{id}")
Student selectById(Integer id);
/**
* 按专业模糊查询学生列表
*/
@Select("SELECT id, name, age, major, score FROM student WHERE major LIKE CONCAT('%', #{keyword}, '%')")
List<Student> selectByMajorLike(String keyword);
/**
* 统计各专业学生人数,返回 Map
* key 为专业名称,value 为人数
*/
@Select("SELECT major AS major, COUNT(*) AS cnt FROM student GROUP BY major")
@MapKey("major") // 注意:此处在返回 Map<String, Integer> 时实际由 SQL 别名控制
List<Map<String, Object>> countByMajor();
}
测试调用代码
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;
import java.util.List;
import java.util.Map;
public class SelectTest {
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. 查询所有学生
System.out.println("=== 查询所有学生 ===");
List<Student> all = mapper.selectAll();
all.forEach(System.out::println);
// 2. 根据 ID 查询
System.out.println("\n=== 根据 ID 查询 ===");
Student stu = mapper.selectById(1);
System.out.println(stu);
// 3. 模糊查询专业包含'软件'的学生
System.out.println("\n=== 模糊查询 ===");
List<Student> software = mapper.selectByMajorLike("软件");
software.forEach(System.out::println);
// 4. 按专业统计人数
System.out.println("\n=== 按专业统计 ===");
List<Map<String, Object>> stats = mapper.countByMajor();
stats.forEach(System.out::println);
session.close();
}
}
实际执行结果
控制台 SQL 输出
=== 查询所有学生 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectAll - ==> Preparing: SELECT id, name, age, major, score FROM student
[main] DEBUG com.flying.mapper.StudentMapper.selectAll - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.selectAll - <== Total: 5
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.5}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.0}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.0}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.5}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.0}
=== 根据 ID 查询 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectById - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[main] DEBUG com.flying.mapper.StudentMapper.selectById - ==> Parameters: 1(Integer)
[main] DEBUG com.flying.mapper.StudentMapper.selectById - <== Total: 1
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.5}
=== 模糊查询 ===
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorLike - ==> Preparing: SELECT id, name, age, major, score FROM student WHERE major LIKE CONCAT('%', ?, '%')
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorLike - ==> Parameters: 软件(String)
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorLike - <== Total: 2
Student{id=2, name='白歌', age=21, major='软件工程', score=88.0}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.0}
=== 按专业统计 ===
[main] DEBUG com.flying.mapper.StudentMapper.countByMajor - ==> Preparing: SELECT major AS major, COUNT(*) AS cnt FROM student GROUP BY major
[main] DEBUG com.flying.mapper.StudentMapper.countByMajor - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.countByMajor - <== Total: 3
{major=计算机科学, cnt=2}
{major=软件工程, cnt=2}
{major=信息安全, cnt=1}
查询结果集表格
| 查询类型 | 结果 |
|---|---|
| selectAll | 5 条记录,包含大翔、白歌、小崔、黄俪、李眉 |
| selectById(1) | 1 条记录:大翔 |
| selectByMajorLike("软件") | 2 条记录:白歌、李眉 |
| countByMajor | 计算机科学:2, 软件工程:2, 信息安全:1 |
分析
@Select的value中,#{id}会被预编译为?占位符,有效防止 SQL 注入- 返回
List<Student>时,MyBatis 自动将每条结果映射为Student对象并收集到列表 - 返回
Map时,SQL 中的别名会成为 Map 的 key,适合快速统计场景 - 模糊查询使用了 MySQL 的
CONCAT函数拼接%,保证参数化安全
易错场景 / 常见误区
| 误区 | 错误示例 | 正解 |
|---|---|---|
用 ${} 做条件参数 | WHERE id = ${id} | 使用 #{id},防止 SQL 注入 |
模糊查询直接拼接 % | LIKE '%#{keyword}%' | 使用 CONCAT('%', #{keyword}, '%') 或绑定参数时带 % |
| 返回 Map 时不写别名 | SELECT major, COUNT(*) FROM ... | 给列起别名 AS major,否则 Map key 可能不统一 |
| 认为注解不支持 JOIN | — | @Select 完全支持 JOIN,只是长 SQL 可读性较差 |
面试考点
Q1:@Select 中的 #{} 和 ${} 有什么区别?
#{}使用预编译参数(?占位符),安全且能防止 SQL 注入;${}是字符串直接替换,用于表名、列名等不能预编译的场景,存在注入风险,条件查询中应优先使用#{}。
Q2:注解方式和 XML 方式可以混用吗?
可以。MyBatis 允许同一个 Mapper 接口中部分方法用注解、部分方法映射到 XML。但需注意:如果注解和 XML 同时定义了同一个方法的 SQL,MyBatis 会抛出异常,要求唯一映射。
Q3:@Select 返回 List 和返回单个对象在写法上有区别吗?
Mapper 接口的返回类型声明有区别(
List<Student>vsStudent),但@Select注解本身写法完全一致。MyBatis 根据接口返回类型自动判断是返回集合还是单个对象。
Q4:注解方式下如何实现动态 SQL?
有三种途径:(1) 在
@Select的字符串中使用<script>标签包裹 XML 动态 SQL;(2) 使用@SelectProvider指定 Provider 类动态生成 SQL;(3) 使用 MyBatis 3.5.1+ 的ProviderMethodResolver简化配置。
小结
@Select 是 MyBatis 注解查询的基石,它将 SQL 与 Mapper 接口方法绑定,适合 SQL 相对固定的查询场景。掌握 #{} 参数绑定、返回类型声明、以及别名与 Map 的映射关系,即可覆盖日常 80% 的查询需求。对于复杂动态条件,可继续学习 @SelectProvider。
下一章引子
查询之后通常是数据新增。在注解开发中,@Insert 负责替代 XML 的 <insert> 完成数据插入,并且常与 @Options 配合实现主键自动回填。下一节将详细讲解 @Insert 的用法。