数据库表结构
CREATE TABLE `teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tname` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`Sname` varchar(255) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`t_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
- student 表:存储学生信息,包含字段
id
(主键)、Sname
(姓名)、sex
(性别)、age
(年龄)、t_id
(关联教师 ID) - teacher 表:存储教师信息,包含字段
id
(主键)、tname
(教师姓名)
一 创建新实体类(最常用)
MyBatis 映射配置
<select id="findStudentTeacher" resultType="com.qcby.entity.StudentTeacher">
select student.*,teacher.Tname from student
LEFT JOIN teacher on student.t_id=teacher.id ;
</select>
实体类设计
public class StudentTeacher {
private Integer id; // 学生ID
private String Sname; // 学生姓名
private String sex; // 学生性别
private String age; // 学生年龄(注意:数据库为int,此处应为Integer更合适)
private Integer t_id; // 关联教师ID
private String Tname; // 教师姓名
// getters/setters/toString
}
- 字段映射:StudentTeacher 类的属性与查询结果字段一一对应
- 类型问题:
age
字段在数据库中为int
,但实体类中定义为String
,可能导致类型不匹配(需注意)
测试方法
@Test
public void findStudentTeacher(){
List<StudentTeacher> studentTeachers = mapper.findStudentTeacher();
for(StudentTeacher studentTeacher : studentTeachers){
System.out.println(studentTeacher.toString());
}
}
- 功能:调用 Mapper 接口方法执行查询,并打印所有学生 - 教师关联信息
- 输出:
StudentTeacher{id=1, Sname='张三', sex='男', age='18', t_id=1, Tname='张老师'} StudentTeacher{id=2, Sname='李四', sex='女', age='18', t_id=1, Tname='张老师'} StudentTeacher{id=5, Sname='小红', sex='女', age='20', t_id=2, Tname='李老师'} // ...其他学生记录
- 查询类型:左外连接(LEFT JOIN),确保即使学生没有关联教师(t_id 为 NULL)也会被查询出来
- 查询字段:
student.*
(所有学生字段) +teacher.Tname
(教师姓名) - 关联条件:
student.t_id = teacher.id
,通过学生表的外键关联教师表主键
二 关联映射
1. SQL 查询与关联逻辑
<select id="findStudentTeacher1" resultMap="StudentTeacher">
select student.*, teacher.Tname
from student
LEFT JOIN teacher on student.t_id = teacher.id;
</select>
- 查询类型:左外连接(LEFT JOIN),确保即使学生没有关联教师(
t_id
为 NULL)也会被查询出来。 - 返回字段:
student.*
:学生表的所有字段(id
,Sname
,sex
,age
,t_id
)。teacher.Tname
:教师姓名(需注意表别名一致性,原表中字段名为tname
,查询中使用了Tname
作为别名)。
2. ResultMap 配置
<resultMap id="StudentTeacher" type="com.qcby.entity.student">
<id property="id" column="id"/>
<result property="Sname" column="Sname"/>
<result property="sex" column="sex"/>
<result property="age" column="age"/>
<result property="t_id" column="t_id"/>
<association property="teacher" javaType="com.qcby.entity.Teacher">
<result property="Tname" column="Tname"/>
</association>
</resultMap>
- 核心配置:
- type:主实体类为
com.qcby.entity.student
(注意类名首字母应大写,规范为Student
)。 - association:处理一对一关联,将查询结果中的教师信息映射到
student
类的teacher
属性中。property="teacher"
:student
类中定义的Teacher
类型属性。javaType
:指定teacher
属性的类型为com.qcby.entity.Teacher
。- 内部的
<result>
标签:将查询结果的Tname
列映射到Teacher
类的Tname
属性(需确保Teacher
类中存在该属性)。
- type:主实体类为
3. 实体类结构
// Student类
public class Student {
private Integer id;
private String Sname;
private String sex;
private Integer age; // 注意:原代码中age为String,建议改为Integer与数据库类型匹配
private Integer t_id;
private Teacher teacher; // 关联的教师对象
// getters/setters
}
// Teacher类
public class Teacher {
private Integer id; // 虽然查询未返回teacher.id,但建议保留
private String Tname; // 与查询结果中的Tname列映射
// getters/setters
}
4. 执行结果与映射规则
@Test
public void findStudentTeacher1() {
List<Student> studentTeachers = mapper.findStudentTeacher1();
for (Student student : studentTeachers) {
System.out.println(student);
// 若学生有关联教师,可通过 student.getTeacher().getTname() 访问教师姓名
}
}
数据流向:
- SQL 查询返回学生和教师的组合结果集。
- MyBatis 根据
resultMap
配置:- 将学生字段(
id
,Sname
,sex
,age
,t_id
)映射到Student
对象。 - 将教师字段(
Tname
)映射到Student
对象的teacher
属性(类型为Teacher
)。
- 将学生字段(
- 若学生无关联教师(
t_id
为 NULL):teacher
属性会被初始化为null
,而非空的Teacher
对象。
5. 潜在问题与优化建议
-
字段映射问题:
teacher.id
缺失:查询中未返回teacher.id
,但Teacher
类通常应有该属性。建议在 SQL 中增加teacher.id as teacher_id
,并在resultMap
中添加<id property="id" column="teacher_id"/>
。- 大小写不一致:若数据库字段为
tname
,而查询中使用Tname
作为别名,需确保一致性。
-
类型匹配:
- 数据库中
age
为int
,实体类中建议使用Integer
而非String
。
- 数据库中
-
优化 ResultMap:
<resultMap id="StudentTeacher" type="com.qcby.entity.Student">
<id property="id" column="id"/>
<result property="sname" column="Sname"/> <!-- 建议属性名小写开头 -->
<result property="sex" column="sex"/>
<result property="age" column="age"/>
<result property="tId" column="t_id"/> <!-- 驼峰命名更规范 -->
<association property="teacher" javaType="com.qcby.entity.Teacher">
<id property="id" column="teacher_id"/> <!-- 增加教师ID映射 -->
<result property="name" column="Tname"/> <!-- 建议属性名改为name -->
</association>
</resultMap>
<!-- 对应的SQL调整 -->
<select id="findStudentTeacher1" resultMap="StudentTeacher">
SELECT
s.id, s.Sname, s.sex, s.age, s.t_id,
t.id AS teacher_id, t.tname AS Tname <!-- 显式指定别名 -->
FROM student s
LEFT JOIN teacher t ON s.t_id = t.id;
</select>
6. 输出
通过这种关联映射,MyBatis 将关系型数据转换为对象嵌套结构,方便在 Java 代码中直接访问关联对象(如student.getTeacher().getName()
)。
ResultMap
- 定义:
ResultMap
是 MyBatis 中最强大的元素,用于定义 SQL 查询结果与 Java 对象属性之间的映射关系。 - 适用场景:
- 表字段名与对象属性名不一致。
- 处理复杂的关联查询(一对一、一对多、多对多)。
- 自定义类型转换。
基础标签
<resultMap>
:根标签,定义映射规则。id
:唯一标识,供<select>
引用。type
:映射的目标 Java 类。
<id>
:主键映射(提高性能)。<result>
:普通字段映射。<association>
:一对一关联映射。<collection>
:一对多关联映射。
三多表查询;分步实现
<!--多表查询;分步实现-->
<!--select *from student--><!--将第一个查询出来的t_1d字段的值写入到第二个sql当中-->
<!--select *from teacher where id = #{t_id}-->
<select id="findStudentTeacher2" resultMap="StudentTeacher2">
SELECT * FROM student
</select>
<resultMap id="StudentTeacher2" type="com.qcby.entity.student">
<id property="id" column="id"/>
<result property="Sname" column="Sname"/>
<result property="sex" column="sex"/>
<result property="age" column="age"/>
<result property="t_id" column="t_id"/>
<!-- column:传值的作用 select:sql方法调用-->
<association property="teacher" javaType="com.qcby.entity.Teacher" column="t_id" select="getTeacher" />
</resultMap>
<select id="getTeacher" resultType="com.qcby.entity.Teacher">
SELECT * FROM teacher where id=#{t_id}
</select>
主查询:findStudentTeacher2
<select id="findStudentTeacher2" resultMap="StudentTeacher2">
SELECT * FROM student
</select>
- 功能:查询所有学生记录
- 结果映射:使用
StudentTeacher2
这个 resultMap 处理结果
2. 结果映射配置:StudentTeacher2
<resultMap id="StudentTeacher2" type="com.qcby.entity.student">
<id property="id" column="id"/>
<result property="Sname" column="Sname"/>
<!-- 其他基本字段映射 -->
<association property="teacher" javaType="com.qcby.entity.Teacher"
column="t_id" select="getTeacher" />
</resultMap>
- 关键点:
<association>
标签定义了学生与教师的关联关系property="teacher"
:将查询结果映射到 Student 类的 teacher 属性javaType="com.qcby.entity.Teacher"
:指定关联对象的类型column="t_id"
:将主查询中的 t_id 字段值作为参数传递给子查询select="getTeacher"
:指定子查询的方法名
3. 子查询:getTeacher
<select id="getTeacher" resultType="com.qcby.entity.Teacher">
SELECT * FROM teacher where id=#{t_id}
</select>
- 功能:根据教师 ID 查询教师信息
- 参数来源:来自主查询中传递的
t_id
值 - 结果处理:直接映射到 Teacher 类(自动匹配属性名和字段名)
@Test
public void findStudentTeacher2(){
List<student> studentTeachers = mapper.findStudentTeacher2();
for (student studentTeacher:studentTeachers){
System.out.println(studentTeacher.toString());
}
}
- 执行流程:
- 调用
findStudentTeacher2()
方法查询所有学生 - 对于每个学生记录,MyBatis 会自动触发
getTeacher
查询其关联教师 - 将教师对象设置到 Student 的 teacher 属性中
- 最终返回一个包含完整教师信息的学生列表
- 调用
四一对多(一个老师对多个学生)
<!--查询老师的学生-->
<select id="FindTeacherStudents" resultMap="TeacherStudents" >
select teacher.*,student.* from student RIGHT JOIN teacher on student.t_id=teacher.id ;
</select>
<resultMap id="TeacherStudents" type="com.qcby.entity.Teacher">
<id property="id" column="id"/>
<result property="Tname" column="Tanme"/>
<!--ofType泛型里的类型-->
<collection property="students" ofType="com.qcby.entity.student">
<id property="id" column="id"/>
<result property="Sname" column="Sname"/>
<result property="sex" column="sex"/>
<result property="age" column="age"/>
<result property="t_id" column="t_id"/>
</collection>
</resultMap>
@Test
public void FindTeacherStudents(){
List<Teacher> teachers=mapper.FindTeacherStudents();
for(Teacher teacher:teachers){
System.out.println(teacher.toString());
}
}
分步解析
1. SQL 查询部分
<select id="FindTeacherStudents" resultMap="TeacherStudents">
select teacher.*, student.*
from student RIGHT JOIN teacher
on student.t_id = teacher.id;
</select>
- 功能:通过右外连接查询所有老师及其关联的学生
- 表关系:
student.t_id
(学生表的外键)关联teacher.id
(老师表主键) - 结果集:返回的每条记录包含一个老师和一个学生的组合(若老师无学生,学生字段为 NULL)
2. 结果映射配置
<resultMap id="TeacherStudents" type="com.qcby.entity.Teacher">
<id property="id" column="id"/>
<result property="Tname" column="Tanme"/> <!-- 注意:可能存在字段名拼写错误 -->
<collection property="students" ofType="com.qcby.entity.student">
<id property="id" column="id"/>
<result property="Sname" column="Sname"/>
<result property="sex" column="sex"/>
<result property="age" column="age"/>
<result property="t_id" column="t_id"/>
</collection>
</resultMap>
- 主映射:将结果映射到
Teacher
类 - 字段映射:
id
→Teacher.id
Tname
→Teacher.Tname
(注意:SQL 列名可能应为tname
)
- 集合映射:
property="students"
:将学生列表映射到Teacher.students
属性ofType="student"
:指定集合元素类型为Student
- 内部的
<result>
标签定义学生对象的字段映射
3. Java 测试代码
@Test
public void FindTeacherStudents() {
List<Teacher> teachers = mapper.FindTeacherStudents();
for(Teacher teacher : teachers) {
System.out.println(teacher.toString());
}
}
- 执行流程:
- 调用
FindTeacherStudents()
方法执行 SQL 查询 - MyBatis 根据
resultMap
配置将结果集转换为Teacher
对象列表 - 每个
Teacher
对象的students
属性包含其关联的学生列表 - 打印每个老师及其学生信息
- 调用
关键技术点
1. 右外连接(RIGHT JOIN)
- 作用:确保即使老师没有学生,也会出现在结果集中(学生字段为 NULL)
- 替代方案:若只需查询有学生的老师,可使用
INNER JOIN
2. 集合映射(<collection>
)
- 核心机制:MyBatis 通过相同的
teacher.id
将多条记录合并到同一个Teacher
对象 - 与分步查询的区别:
- 此方案通过单条 SQL 查询所有数据(适合数据量小的场景)
- 分步查询会触发多次 SQL(适合延迟加载或大数据量场景)
3. 字段名冲突处理
- 当两个表存在相同字段名(如
id
)时,MyBatis 会自动处理:- 主对象(Teacher)优先使用前面的列(
teacher.id
) - 嵌套对象(Student)使用后面的列(
student.id
)
- 主对象(Teacher)优先使用前面的列(
懒加载
MyBatis 的懒加载(延迟加载)是一种优化机制,允许在真正需要关联对象时才进行数据库查询,而不是在主对象加载时立即加载所有关联数据。这种策略可以显著提升性能,尤其是在处理复杂对象关系时。
核心原理
MyBatis 通过动态代理实现懒加载:
- 当主对象(如
User
)被加载时,其关联对象(如Order
)会被替换为一个代理对象。 - 代理对象在首次被访问时(例如调用
user.getOrders()
),才会触发实际的 SQL 查询,加载关联数据。
配置步骤
1. 全局配置(mybatis-config.xml
)
启用懒加载并设置 aggressiveLazyLoading 为 false(避免触发所有 getter):
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
2. 映射文件配置
在关联查询中使用 <association>
(一对一)或 <collection>
(一对多)标签,并设置 fetchType="lazy"
:
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<collection
property="orders"
ofType="Order"
column="id"
select="selectOrdersByUserId"
fetchType="lazy"/> <!-- 显式指定懒加载 -->
</resultMap>
3. Java 接口映射(注解方式)
public interface UserMapper {
@Results({
@Result(property = "orders", column = "id",
many = @Many(select = "selectOrdersByUserId", fetchType = FetchType.LAZY))
})
@Select("SELECT * FROM users WHERE id = #{id}")
User selectUserById(int id);
}
注意事项
-
会话生命周期
懒加载的代理对象需要在 SqlSession 关闭前 被访问,否则会抛出SessionAlreadyClosedException
。可通过OpenSessionInView
模式解决(如 Spring 的OpenSessionInViewInterceptor
)。 -
序列化问题
懒加载的代理对象无法直接序列化(如返回 JSON),需确保在序列化前已加载所有数据。可通过@JsonIgnore
排除未加载的属性,或使用Hibernate.initialize()
强制初始化。 -
嵌套查询性能
懒加载可能导致 N+1 查询问题(主查询 1 次,关联查询 N 次),需结合BatchExecutor
或二级缓存优化。
示例代码
// 主对象
public class User {
private Integer id;
private String name;
private List<Order> orders; // 延迟加载的关联对象
// getters/setters
}
// 测试懒加载
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserById(1); // 此时 orders 是代理对象
// 首次访问 orders 时触发实际查询
System.out.println(user.getOrders().size());
}
禁用懒加载的场景
- 关联数据一定会被使用,提前加载更高效。
- 需要在会话关闭后访问关联对象。
- 使用二级缓存时,懒加载可能失效。
通过合理配置懒加载,MyBatis 可以在保证数据完整性的同时,大幅提升系统性能。