MyBatis的一对多关联关系
本文修订于2019年3月17日
SQL脚本
在实际项目开发中,一对多是非常常见的关系,比如,一个班级可以有多个学生,一个学生只能属于一个班级,班级和学生是一对多的关系,而学生和班级是多对一的关系。数据库中一对多关系通常使用主外键关联,外键列应该在多方,即多方维护关系。下面我们就用一个简单示例来看看MyBatis怎么处理一对多关系。首先,给在数据库创建两个表tb_clazz 和tb_student,并插入测试数据。SQL脚本如下:
drop table if exists tb_clazz;
CREATE TABLE tb_clazz(
id INT PRIMARY KEY AUTO_INCREMENT,
CODE VARCHAR(18),
NAME VARCHAR(18)
);
INSERT INTO tb_clazz(CODE,NAME) VALUES('201803','计算机专业');
drop table if exists tb_student;
CREATE TABLE tb_student(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(18),
sex VARCHAR(18),
age INT,
clazz_id INT,
FOREIGN KEY(clazz_id) REFERENCES tb_clazz(id)
);
INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('马云','男',23,1) ;
INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('李飞飞','女',23,1) ;
INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('马化腾','男',23,1) ;
INSERT INTO tb_student (NAME,sex,age,clazz_id) VALUES('柳青','女',23,1) ;
提示:
tb_student表的clazz_id作为外键参照tb_clazz表的主键id。
实体类
在数据库中执行SQL脚本,完成创建数据库和表的操作。接下来,创建一个Clazz对象和一个Student对象分别映射tb_clazz和tb_student表。
班级和学生是一对多的关系,即一个班级可以有多个学生。在Clazz类当中定义了一个students属性,该属性是一个List集合,用来映射一对多的关联关系,表示一个班级有多个学生。
import java.io.Serializable;
import java.util.List;
public class Clazz implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 班级id,主键
private String code; // 班级编号
private String name; // 班级名称
// 班级和学生是一对多的关系,即一个班级可以有多个学生
private List<Student> students;
public Clazz()
{
super();
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getCode()
{
return code;
}
public void setCode(String code)
{
this.code = code;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public List<Student> getStudents()
{
return students;
}
public void setStudents(List<Student> students)
{
this.students = students;
}
@Override
public String toString()
{
return "Clazz [id=" + id + ", code=" + code + ", name=" + name + "]";
}
}
班级和学生是一对多的关系,即一个班级可以有多个学生。在Clazz类当中定义了一个students属性,该属性是一个List集合,用来映射一对多的关联关系,表示一个班级有多个学生。
import java.io.Serializable;
public class Student implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 学生id,主键
private String name; // 姓名
private String sex; // 性别
private Integer age; // 年龄
// 学生和班级是多对一的关系,即一个学生只属于一个班级
private Clazz clazz;
public Student()
{
super();
}
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 String getSex()
{
return sex;
}
public void setSex(String sex)
{
this.sex = sex;
}
public Integer getAge()
{
return age;
}
public void setAge(Integer age)
{
this.age = age;
}
public Clazz getClazz()
{
return clazz;
}
public void setClazz(Clazz clazz)
{
this.clazz = clazz;
}
@Override
public String toString()
{
return "Student [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
学生和班级是多对一的关系,即一个学生只属于一个班级。在Student类当中定义了一个clazz属性,该属性是一个Clazz类型,用来映射多对一的关联关系,表示该学生所属的班级。
映射文件
<mapper namespace="cn.mybatis.mydemo2.mapper.ClazzMapper">
<!-- 根据id查询班级信息,返回resultMap -->
<select id="selectClazzById" parameterType="int"
resultMap="clazzResultMap">
SELECT * FROM tb_clazz WHERE id = #{id}
</select>
<!-- 映射Clazz对象的resultMap -->
<resultMap type="cn.mybatis.mydemo2.domain.Clazz"
id="clazzResultMap">
<id property="id" column="id" />
<result property="code" column="code" />
<result property="name" column="name" />
<!-- 一对多关联映射:collection fetchType="lazy"表示懒加载 -->
<collection property="students" javaType="ArrayList"
column="id" ofType="cn.mybatis.mydemo2.domain.Student"
select="cn.mybatis.mydemo2.mapper.StudentMapper.selectStudentByClazzId"
fetchType="lazy">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="sex" column="sex" />
<result property="age" column="age" />
</collection>
</resultMap>
</mapper>
ClazzMapper.xml中定义了一个<select.../>,其根据id查询班级信息。由于Clazz类除了简单的属性id、code、name 之外,还有一个关联对象students,所以返回的是一个名为clazzResultMap的resultMap。
由于student是一个List集合,所以clazzResultMap中使用了<collection.../>元素映射一对多的关联关系,select属性表示会使用column属性的id值作为参数执行StudentMapper中定义的selectStudentByClazz查询该班级对应的所有学生数据,查询出的数据将被封装到property表示的students对象当中。
还使用了一个新的属性fetchType,该属性的取值有eager和lazy,eager表示立即加载,即查询Clazz对象的时候,会立即执行关联的selectStudentByClazzId中定义的SQL语句去查询班级的所有学生;lazy表示懒加载,其不会立即发送SQL语句去查询班级的所有学生,而是等到需要使用到班级的students属性时,才会发送SQL语句去查询班级的所有学生。fetch机制更多的是为了性能考虑,如果查询班级时确定会访问班级的所有学生,则该属性应该设置为eager;如果查询班级时只是查询班级信息,有可能不会访问班级的所有学生,则该属性应该设置为lazy。正常情况下,一对多所关联的集合对象,都应该被设置成lazy。
使用懒加载还需要在mybatis-config.xm 中增加如下配置:
<settings>
<setting name="logImpl" value="LOG4J"/>
<!-- 要使延迟加载生效必须配置下面两个属性 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
lazyLoadingEnabled属性表示延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认为false。
agressiveLazyLoading属性启用时,会使带有延迟加载属性的对象立即加载; 反之,每种属性将会按需加载。默认为true,所以这里需要设置成false。
<mapper namespace="cn.mybatis.mydemo2.mapper.StudentMapper">
<!-- 根据id查询学生信息,多表连接,返回resultMap -->
<select id="selectStudentById" parameterType="int"
resultMap="studentResultMap">
SELECT * FROM tb_clazz c,tb_student s
WHERE c.id = s.clazz_id
AND s.id = #{id}
</select>
<!-- 根据班级id查询学生信息,返回resultMap -->
<select id="selectStudentByClazzId" parameterType="int"
resultMap="studentResultMap">
SELECT * FROM tb_student WHERE clazz_id = #{id}
</select>
<!-- 映射Student对象的resultMap -->
<resultMap type="cn.mybatis.mydemo2.domain.Student"
id="studentResultMap">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="sex" column="sex" />
<result property="age" column="age" />
<!-- 多对一关联映射:association -->
<association property="clazz"
javaType="cn.mybatis.mydemo2.domain.Clazz">
<id property="id" column="id" />
<result property="code" column="code" />
<result property="name" column="name" />
</association>
</resultMap>
</mapper>
StudentMapper.xm 中定义了一个<select id="selectStudentById".../> 其会根据学生id查询学生信息,由于Student类除了简单的属性id、name、sex和age之外,还有一个关联对象clazz,所以它返回的是一个名为clazzResultMap的resultMap。clazzResutMap中使用了<association.../>元素映射多对一的关联关系,因为<select id="selectStudenById".../>的SQL 语句是一条多表连接,关联tb_clazz表的同时查询了班级数据,所以<association.../>只是简单地装载数据。
在实际开发中,一对多关系通常映射为集合对象,而由于多方的数据量可能很大,所以通常使用懒加载; 而多对一只是关联到一个对象,所以通常使用多表连接直接把数据提取出来。
StudentMapper.xml 中还定义了一个<select id="selectStudentByClazzId".../>其会根据班级id查询所有学生信息,该查询用于ClazzMapper.xml 中的关联查询。
映射接口
import cn.mybatis.mydemo2.domain.Clazz;
public interface ClazzMapper
{
// 根据id查询班级信息
Clazz selectClazzById(Integer id);
}
import cn.mybatis.mydemo2.domain.Student;
public interface StudentMapper
{
// 根据id查询学生信息
Student selectStudentById(Integer id);
}
测试类
public class App
{
public static void main(String[] args) throws Exception
{
// 读取mybatis-config.xml文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 初始化mybatis,创建SqlSessionFactory类的实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建Session实例
SqlSession session = sqlSessionFactory.openSession();
App app = new App();
app.testSelectClazzById(session);
// t.testSelectStudentById(session);
// 提交事务
session.commit();
// 关闭Session
session.close();
}
// 测试一对多,查询班级Clazz(一)的时候级联查询学生Student(多)
public void testSelectClazzById(SqlSession session)
{
// 获得ClazzMapper接口的代理对象
ClazzMapper cm = session.getMapper(ClazzMapper.class);
// 调用selectClazzById方法
Clazz clazz = cm.selectClazzById(1);
// 查看查询到的clazz对象信息
System.out.println(clazz.getId() + " " + clazz.getCode() + " " + clazz.getName());
// 查看clazz对象关联的学生信息
List<Student> students = clazz.getStudents();
for (Student stu : students)
{
System.out.println(stu);
}
}
// 测试多对一,查询学生Student(多)的时候级联查询 班级Clazz(一)
public void testSelectStudentById(SqlSession session)
{
// 获得StudentMapper接口的代理对象
StudentMapper sm = session.getMapper(StudentMapper.class);
// 调用selectStudentById方法
Student stu = sm.selectStudentById(1);
// 查看查询到的Student对象信息
System.out.println(stu);
// 查看Student对象关联的班级信息
System.out.println(stu.getClazz());
}
}
在APP类中定义了一个testSelectClazzById()方法,该方法用于测试一对多关系,查询班级Clazz(一)的时候级联查询学生Student(多)的信息。运行APP类的main方法,其通过SqlSession的getMapper(Class<Type> t)方法获得mapper接口的代理对象ClazzMapper,调用selectClazzByld方法时会执行ClazzMapper.xml的<select id="selectClazzById" .../>中定义的sql语句。控制台显示如下:
DEBUG [main]==>Preparing: SELECT * PROM tb_clazz WHERE id = ?
DEBUG [main]-->Parameters: 1(Integer)
DEBUG [main]<==Total :1
1 201803 计算机专业
可以看到,MyBatis只是执行了查询班级的SQL语句,由<select id="selectClazzById" .../>中的<colletion fethType=lazy">使用的是懒加载,因此,当没有使用到关联的学生对象时,并没有发送查询学生的SQL语句。修改testSelectClazzByld方法,增加访问关联学生的代码:
// 查看clazz对象关联的学生信息
List<Student> students = clazz.getStudents();
for (Student stu : students)
{
System.out.println(stu);
}
再次运行APP类的main方法。控制台显示如下:
DEBUG [main]--> Preparing: SBIECT * FRON tb_clazz WHERE id = ?
DEBUG [main]==> Parameters: 1(Integer)
DEBUG [main]<== Total :
1 201803 计算机专业
DEBUG [main]--> Preparing: SELECT * FRON tb_student WHERE clazz id = ?
DEBUG [mainj--> Parameters: 1(Integer)
DEBUG [main]<== Total: 4
student [id=1,name=马云,sex=男,age=23]
student [id=2,name=李飞飞,sex=女,age=23]
Student [id=3,name=马化腾,sex=男,age=23]
Student [id=4,name=柳青,sex=女,age=23]
可以看到,MyBatis执行了查询班级的SQL语句之后,又执行了根据clazz_id 查询学生信息的SQL语句。这就是所谓的懒加载。增加一个testSelectStudentBy方法,测试多对一。
在main方法中运行testSelectStudentById方法,控制台显示如下:
DEBUG[main]---> Preparing: SELECT * FRON tb_clazz c,tb_student s WHERE c.id = s.clazz_id AND s.id = ?
DEBUG[main]---> Parameters: 1(Integer)
DEBUG[main]<--- Total:
Student[id=1,name=马云,sex=男,age=23]
Clazz[id=1,code=201803,name=计算机专业]
可以看到,MyBatis执行了一个多表查询语句,并且将查询到的班级信息封装到了学生对象的关联属性中。
如果Clazz类中不添加student属性不知道可行吗?
SELECT * FROM tb_student WHERE clazz_id = #{id}