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

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

@One

导学

本节学习目标:

  • 掌握 @One 注解的基本用法,能够替代 XML 中的 <association> 完成一对一关联映射
  • 理解 @One 的 select 属性,学会通过嵌套查询实现关联对象加载
  • 掌握 fetchType 属性,能够根据业务需求选择立即加载或延迟加载
  • 展示嵌套查询的完整代码,包括主查询 Mapper 和关联查询 Mapper

定义

@One 是 MyBatis 提供的一对一关联映射注解,用在 @Result 注解的 one 属性中,用于声明某个属性对应的是一个单一关联对象。它对应 XML 映射文件中的 <association> 元素。

痛点解决:在查询学生信息时,通常需要同时获取其导师信息(如导师姓名、研究方向)。如果不使用关联映射,开发者需要手动执行两次查询并自行组装对象。@One 通过声明嵌套查询,让 MyBatis 自动完成关联对象的查询和赋值,实现对象图的完整加载。

注解方式 vs XML 方式对比

对比维度@One 注解方式XML <association> 方式
声明位置写在 @Result 的 one 属性中写在 <resultMap> 内部
嵌套查询指定通过 select 属性写方法全限定名通过 select 属性写方法全限定名
加载策略通过 fetchType 指定 LAZY / EAGER通过 fetchType 指定 lazy / eager
可读性关联简单时紧凑关联复杂时 XML 层级更清晰

适用场景建议:一对一关联且关联对象结构简单时优先使用 @One;关联对象极复杂或需要多层嵌套时,建议评估 XML <association>。

适用位置与核心属性

@One 只能用在 @Result 注解的 one 属性中,不能单独标注在方法上。

属性类型必填说明
selectString是嵌套查询方法的全限定名(Mapper 接口全路径.方法名),用于加载关联对象
fetchTypeFetchType否加载策略。FetchType.EAGER 立即加载(默认),FetchType.LAZY 延迟加载

核心原理

MyBatis 解析 @Result 时,如果发现 one 属性不为空,会创建一个嵌套查询的 ResultMapping。执行主查询后,MyBatis 提取关联列的值作为参数,调用 select 属性指定的嵌套查询方法获取关联对象,然后将关联对象赋值给主对象的对应属性。如果 fetchType = LAZY,MyBatis 会生成代理对象,在首次访问该属性时才触发嵌套查询。

完整示例

场景说明

乐途公司学生管理系统中,每名学生有一名导师。需要查询学生信息时,同时加载其导师的姓名和研究方向。数据库中学生表通过 mentor_id 外键关联导师表。

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

CREATE TABLE mentor (
    mentor_id INT PRIMARY KEY AUTO_INCREMENT,
    mentor_name VARCHAR(20),
    research_area VARCHAR(30)
);

CREATE TABLE student (
    stu_id INT PRIMARY KEY AUTO_INCREMENT,
    stu_name VARCHAR(20),
    stu_age INT,
    stu_major VARCHAR(20),
    stu_score DECIMAL(5,2),
    mentor_id INT,
    FOREIGN KEY (mentor_id) REFERENCES mentor(mentor_id)
);

INSERT INTO mentor (mentor_name, research_area) VALUES
('张教授', '人工智能'),
('李教授', '网络安全');

INSERT INTO student (stu_name, stu_age, stu_major, stu_score, mentor_id) VALUES
('大翔', 22, '计算机科学', 95.5, 1),
('白歌', 21, '软件工程', 88.0, 2),
('小崔', 20, '计算机科学', 92.0, 1),
('黄俪', 21, '信息安全', 90.5, 2),
('李眉', 22, '软件工程', 87.0, 1);

当前数据状态:

mentor 表

mentor_idmentor_nameresearch_area
1张教授人工智能
2李教授网络安全

student 表

stu_idstu_namestu_agestu_majorstu_scorementor_id
1大翔22计算机科学95.501
2白歌21软件工程88.002
3小崔20计算机科学92.001
4黄俪21信息安全90.502
5李眉22软件工程87.001

完整的注解接口代码

实体类:Mentor

package com.flying.entity;

public class Mentor {
    private Integer mentorId;
    private String mentorName;
    private String researchArea;

    public Integer getMentorId() { return mentorId; }
    public void setMentorId(Integer mentorId) { this.mentorId = mentorId; }
    public String getMentorName() { return mentorName; }
    public void setMentorName(String mentorName) { this.mentorName = mentorName; }
    public String getResearchArea() { return researchArea; }
    public void setResearchArea(String researchArea) { this.researchArea = researchArea; }

    @Override
    public String toString() {
        return "Mentor{mentorId=" + mentorId + ", mentorName='" + mentorName + "', researchArea='" + researchArea + "'}";
    }
}

实体类:Student(含 Mentor 关联)

package com.flying.entity;

public class Student {
    private Integer stuId;
    private String stuName;
    private Integer stuAge;
    private String stuMajor;
    private Double stuScore;
    private Mentor mentor;

    public Integer getStuId() { return stuId; }
    public void setStuId(Integer stuId) { this.stuId = stuId; }
    public String getStuName() { return stuName; }
    public void setStuName(String stuName) { this.stuName = stuName; }
    public Integer getStuAge() { return stuAge; }
    public void setStuAge(Integer stuAge) { this.stuAge = stuAge; }
    public String getStuMajor() { return stuMajor; }
    public void setStuMajor(String stuMajor) { this.stuMajor = stuMajor; }
    public Double getStuScore() { return stuScore; }
    public void setStuScore(Double stuScore) { this.stuScore = stuScore; }
    public Mentor getMentor() { return mentor; }
    public void setMentor(Mentor mentor) { this.mentor = mentor; }

    @Override
    public String toString() {
        return "Student{stuId=" + stuId + ", stuName='" + stuName + "', stuAge=" + stuAge +
               ", stuMajor='" + stuMajor + "', stuScore=" + stuScore + ", mentor=" + mentor + "}";
    }
}

Mapper 接口:MentorMapper(嵌套查询)

package com.flying.mapper;

import com.flying.entity.Mentor;
import org.apache.ibatis.annotations.Select;

public interface MentorMapper {

    /**
     * 根据导师 ID 查询导师信息
     * 该方法将被 StudentMapper 中的 @One 引用
     */
    @Select("SELECT mentor_id, mentor_name, research_area FROM mentor WHERE mentor_id = #{mentorId}")
    Mentor selectById(Integer mentorId);
}

Mapper 接口:StudentMapper(主查询 + 关联映射)

package com.flying.mapper;

import com.flying.entity.Student;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.One;
import java.util.List;

public interface StudentMapper {

    @Select("SELECT stu_id, stu_name, stu_age, stu_major, stu_score, mentor_id FROM student")
    @Results(id = "studentWithMentorMap", value = {
            @Result(column = "stu_id", property = "stuId", id = true),
            @Result(column = "stu_name", property = "stuName"),
            @Result(column = "stu_age", property = "stuAge"),
            @Result(column = "stu_major", property = "stuMajor"),
            @Result(column = "stu_score", property = "stuScore"),
            @Result(column = "mentor_id", property = "mentor",
                    one = @One(select = "com.flying.mapper.MentorMapper.selectById", fetchType = FetchType.EAGER))
    })
    List<Student> selectAllWithMentor();
}

测试调用代码

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;

public class OneTest {
    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);

        System.out.println("=== 查询所有学生(含导师信息)===");
        List<Student> all = mapper.selectAllWithMentor();
        all.forEach(System.out::println);

        session.close();
    }
}

实际执行结果

控制台 SQL 输出

=== 查询所有学生(含导师信息)===
[main] DEBUG com.flying.mapper.StudentMapper.selectAllWithMentor - ==>  Preparing: SELECT stu_id, stu_name, stu_age, stu_major, stu_score, mentor_id FROM student
[main] DEBUG com.flying.mapper.StudentMapper.selectAllWithMentor - ==> Parameters:
[main] DEBUG com.flying.mapper.StudentMapper.selectAllWithMentor - <==      Total: 5
[main] DEBUG com.flying.mapper.MentorMapper.selectById - ==>  Preparing: SELECT mentor_id, mentor_name, research_area FROM mentor WHERE mentor_id = ?
[main] DEBUG com.flying.mapper.MentorMapper.selectById - ==> Parameters: 1(Integer)
[main] DEBUG com.flying.mapper.MentorMapper.selectById - <==      Total: 1
[main] DEBUG com.flying.mapper.MentorMapper.selectById - ==>  Preparing: SELECT mentor_id, mentor_name, research_area FROM mentor WHERE mentor_id = ?
[main] DEBUG com.flying.mapper.MentorMapper.selectById - ==> Parameters: 2(Integer)
[main] DEBUG com.flying.mapper.MentorMapper.selectById - <==      Total: 1
Student{stuId=1, stuName='大翔', stuAge=22, stuMajor='计算机科学', stuScore=95.5, mentor=Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}}
Student{stuId=2, stuName='白歌', stuAge=21, stuMajor='软件工程', stuScore=88.0, mentor=Mentor{mentorId=2, mentorName='李教授', researchArea='网络安全'}}
Student{stuId=3, stuName='小崔', stuAge=20, stuMajor='计算机科学', stuScore=92.0, mentor=Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}}
Student{stuId=4, stuName='黄俪', stuAge=21, stuMajor='信息安全', stuScore=90.5, mentor=Mentor{mentorId=2, mentorName='李教授', researchArea='网络安全'}}
Student{stuId=5, stuName='李眉', stuAge=22, stuMajor='软件工程', stuScore=87.0, mentor=Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}}

查询结果集表格

stu_idstu_namementor_idmentor 对象
1大翔1Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}
2白歌2Mentor{mentorId=2, mentorName='李教授', researchArea='网络安全'}
3小崔1Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}
4黄俪2Mentor{mentorId=2, mentorName='李教授', researchArea='网络安全'}
5李眉1Mentor{mentorId=1, mentorName='张教授', researchArea='人工智能'}

分析

  • @One(select = "com.flying.mapper.MentorMapper.selectById") 指定了嵌套查询方法的全限定名,MyBatis 通过 mentor_id 列的值作为参数调用该方法
  • fetchType = EAGER 表示立即加载,主查询执行后会立刻发起嵌套查询。如果改为 LAZY,则只有在访问 student.getMentor() 时才会触发查询
  • 嵌套查询会产生 N+1 问题:本例中查询 5 名学生,产生了 1 次主查询 + 2 次嵌套查询(因为只有 2 个不同的 mentor_id)。在数据量大时,应考虑 JOIN 查询替代嵌套查询

易错场景 / 常见误区

误区错误示例正解
select 属性写错方法名select = "MentorMapper.selectById"必须写全限定名 com.flying.mapper.MentorMapper.selectById
关联列名与嵌套查询参数名不匹配column = "mentor_id" 但嵌套方法参数是 id确保嵌套查询方法的参数类型能接受关联列的值
忘记给关联对象属性写 SetterMentor 类无 setMentor关联对象必须有 Setter,否则 MyBatis 无法赋值
认为 EAGER 一定比 LAZY 好所有关联都用 EAGER关联频繁访问时用 EAGER,偶尔访问时用 LAZY 减少不必要查询

面试考点

Q1:@One 的 select 属性为什么要写全限定名?

MyBatis 需要通过全限定名在 Configuration 中定位到唯一的 MappedStatement。如果只写方法名,不同 Mapper 中可能存在同名方法,导致解析歧义。

Q2:@One 嵌套查询会产生什么问题?如何优化?

会产生 N+1 查询问题:查询 N 条主记录,可能触发 N 次嵌套查询。优化方式:(1) 改用 JOIN 查询一次性获取所有数据;(2) 开启延迟加载 fetchType = LAZY,只在需要时查询;(3) 使用缓存减少重复嵌套查询。

Q3:fetchType 的 LAZY 在注解方式下如何生效?

需要满足两个条件:(1) fetchType = FetchType.LAZY;(2) 全局配置中 aggressiveLazyLoading = false 且 lazyLoadingEnabled = true。MyBatis 会为关联对象生成 CGLIB 代理,在首次访问属性时触发嵌套查询。

Q4:@One 和 @Many 的核心区别是什么?

@One 表示关联属性是单一对象(一对一、多对一),对应 XML 的 <association>;@Many 表示关联属性是集合(一对多),对应 XML 的 <collection>。两者都通过 select 属性指定嵌套查询,但返回类型一个是对象,一个是集合。

小结

@One 让注解开发也能优雅地处理一对一关联映射。通过 select 指定嵌套查询、fetchType 控制加载策略,可以灵活应对不同业务场景。需要注意 N+1 问题,在性能敏感的场景下评估是否改用 JOIN 查询。

下一章引子

一对一之外,一对多关联更为常见。@Many 注解负责替代 XML 的 <collection> 完成一对多集合映射,常用于班级包含学生列表、订单包含商品列表等场景。下一节将详细讲解 @Many 的用法。

上一页
@Result
下一页
@Many