本文修订于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执行了一个多表查询语句,并且将查询到的班级信息封装到了学生对象的关联属性中。

标签: none


已有 2 条评论

  1. 小白 小白

    如果Clazz类中不添加student属性不知道可行吗?

  2. 李某人 李某人

    SELECT * FROM tb_student WHERE clazz_id = #{id}

添加新评论