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

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

@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 接口的方法 上。

属性类型必填说明
valueString是要执行的 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);

当前数据状态:

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;

    // 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}

查询结果集表格

查询类型结果
selectAll5 条记录,包含大翔、白歌、小崔、黄俪、李眉
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> vs Student),但 @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 的用法。

上一页
本章定位
下一页
@Insert