MyBatis教程大全


 MyBatis SQL 映射

     MyBatis select 标签

     MyBatis 多数据库支持

     MyBatis selectKey 标签作用

     MyBatis @SelectKey注解用法介绍

     MyBatis @SelectKey注解用法详细介绍

     MyBatis keyProperty 属性介绍

     MyBatis insert、update 和 delete 元素

     MyBatis sql 元素

 MyBatis SQL 参数映射

     MyBatis SQL 参数映射

 MyBatis 动态SQL

     MyBatis 动态SQL与数据准备

     MyBatis if 标签

     MyBatis if else 用法

     MyBatis choose、when、otherwise 标签

     MyBatis where 标签

     MyBatis set 标签

     MyBatis foreach 标签

     MyBatis bind 标签

     MyBatis trim 标签

 MyBatis SQL 结果映射

 MyBatis SQL 结果之关系映射

 MyBatis 使用介绍

     MyBatis typeAliases 类型别名

     MyBatis typeHandlers 类型处理器

     MyBatis Transaction 事务接口

     MyBatis transactionManager 事务管理

     SqlSessionFactory 介绍

     MyBatis 核心对象 SqlSession

     MyBatis 初始化 创建 Session 实例

     MyBatis ObjectFactory 对象工厂

     MyBatis缓存机制:一级缓存和二级缓存

     MyBatis 常用注解

 MyBatis 配置文件

     MyBatis 配置文件

 MyBatis 映射

     MyBatis 映射简介

     MyBatis ResultMap 映射

     MyBatis 自动映射

     MyBatis 高级映射

     MyBatis 集合映射

     MyBatis 关联映射

     MyBatis 一对一关联映射

     MyBatis 一对多关联映射

     MyBatis 多对多关联映射

     MyBatis 一对一(注解形式)

     MyBatis 一对多(注解形式)

     MyBatis 多对多(注解形式)

     MyBatis resultMap 元素

 MyBatis 面试题库

     #{}和${}的区别是什么?

     数据库链接中断如何处理?

     数据库插入重复如何处理?

     事务执行过程中宕机的应对处理方式

     Java客户端中的一个Connection问题

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