【MyBatis学习总结(四),注解 & 多对一、一对多】

2,902 阅读10分钟

在前面三篇文章中,我们学习了什么是MyBatis、简单实现了一个MyBatis程序、使用MyBatis实现了CRUD操作、涉及了MyBatis的一些常用配置、ResultMap简单使用、日志功能的实现和介绍、分页实现。

以下是之前文章的地址

(一)

(二)

(三)

本篇文章我们会学习注解的使用、多对一、一对多的两种实现方式。多对一和一对多是比较难以理解的,看起来可能会比较累。

在MyBatis入门文档中有这么几段话。

我想这几段话作为注解的引入十分合适

简单使用注解

使用注解的话我们就不再需要编写mapper.xml文件了

既然没有mapper.xml映射文件,那我们可以通过接口的方式来映射

<!--使用mapper接口的全限定名 实现映射-->
<mappers>
    <mapper class="com.molu.mapper.UserMapper"/>
</mappers>

解决了映射问题,我们在mapper接口中写一个方法并使用注解

public interface UserMapper {
    // 使用select标签的注解形式
    @Select("select * from user")
    List<User> getUsers();
}

可以看到,使用注解的话能省很多事情,我们只需要将映射解决,注解中sql写对就可以进行测试了。

但到这里为止,我们的实体类属性名还是没有改回来的,仍然和数据库中的字段名不一致

而只通过注解 我们没办法配置ResultMap来将二者做一个映射,那可想而知,查询出来的结果password会为null

不要紧,后面我们会使用resultMap注解来将二者串起来

测试类

    @Test
    public void getUsersTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = mapper.getUsers();
        for (User user : users) {
            System.out.println(user);
        }
    }

无法避免的,返回的查询结果 password全部为null。

想解决这个问题也很简单,把mapper.xml写一下就可以了,这种混用的方式 在MyBatis中十分常见。

写mapper.xml时需要注意,必须将mapper.xml和mapper接口放在一个接口中且必须同名。(因为映射绑定是通过mapper接口的全限定名)就像这样:

编写mapper.xml映射文件

在 mapper标签中只写 resultMap标签和具体配置,其他的一概不管。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.molu.mapper.UserMapper">
<resultMap id="UserMap" type="user">
    <result column="pwd" property="password"/>
</resultMap>
</mapper>

将 resultMap id绑定到接口中的 @ResultMap注解中。

public interface UserMapper {
    // 进行resultMap id绑定
    @ResultMap("UserMap")
    @Select({"select * from user"})
    List<User> getUsers();
}

写完这些后再运行就能够解决问题了。

还算省事,当然 你也可以通过前面我说过的 在sql语句中加个 as 取别名快速解决问题。

@Select({"select id,name,pwd as password from user"})

这也就是为什么我开头,要使用MyBatis官方文档这几段话作为引入了。

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。

除了混用配置文件和注解,我们也可以使用纯注解来对其进行映射

@Results注解对应着mapper.xml中的resultMap标签

@Result对应着mapper.xml中的result标签

public interface UserMapper {
    @Results({
		@Result(property = "password", column = "pwd")
    })
    @Select({"select * from user"})
    List<User> getUsers();
}

这样写的话也没什么问题,但如果需要映射的字段和属性太多了就有一点惨不忍睹了.......

到这里注解相关就告一段落了,也没太多好讲的。如果还想了解更多注解相关可以移其他博客,比如

多对一

搭建多对一环境

创建老师表(主表)

CREATE TABLE `teacher` (
  `id` int(3) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8

创建学生表(从表)

CREATE TABLE `student` (
  `id` int(3) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `tid` int(3) NOT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

二者使用物理外键关联,也就是说 student表中的tid 是 teacher表中的id

抛出需求

现在我们想在查询学生信息的同时,还要返回 tid所对应老师的名字

如果使用sql来做,那自然是非常的简单

那如果我们要使用MyBatis该如何操作呢?

尝试实现

编写Teacher实体类

public class Teacher {
    private int id;
    private String name;

    public Teacher() {
    }


    public Teacher(int id, String name) { this.id = id;  this.name = name;}

    public int getId() { return id;}

    public void setId(int id) { this.id = id;}

    public String getName() { return name;}

    public void setName(String name) { this.name = name;}

    @Override
    public String toString() {
        return "Teacher{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

编写Student实体类

public class Student {
    private int id;
    private String name;
    private int tid;
    private String teacherName;

    public Student(int id, String name, int tid, String teacherName) {
        this.id = id;
        this.name = name;
        this.tid = tid;
        this.teacherName = teacherName;
    }
    
//========中间省略生成的getter setter方法========
    
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", tid=" + tid +
                ", teacherName=" + teacherName +
                '}';
    }
}

在mapper接口编写方法

public interface StudentMapper {
    List<Student> getStudent();
}

编写mapper.xml文件

<mapper namespace="com.molu.mapper.StudentMapper">
    <select id="getStudent" resultType="Student">
        select s.id,s.name,s.tid,t.name as teacherName
        from student s
        inner join teacher t
        where s.tid=t.id
    </select>
</mapper>

编写测试类

public class MapperTest {
    @Test
    public void getStudentTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> studentList = mapper.getStudent();
        for (Student student : studentList) {
            System.out.println(student);
        }
        sqlSession.close();
    }
}

测试结果:

很意外的居然可以实现....... ,本来还想通过无法实现 来扩展resultMap对复杂查询的支持。

既然都写到这里了还是扩展一下吧......

按照查询嵌套处理

除了比较直接的写一个连表查询sql

我们还可以写两个查询sql,将其中一个 sql的查询结果,作为另一条 sql的查询结果进行嵌套处理。

  • 首先我们将原本的实体类进行修改
public class Student {
    private int id;
    private String name;
    // private int tid;
    // 不再需要 tid属性
    private Teacher teacher;
    // 将原本的 teacherName 修改为 Teacher对象
  • 写两条查询sql
  • 将getTeacher的查询结果 和 Student实体类中的 teacher属性进行映射
  • 映射后, getStudent查询结果中就会嵌套进 getTeacher的查询结果
<mapper namespace="com.molu.mapper.StudentMapper">
    <select id="getStudent" resultType="Student">
        select * from student
    </select>

    <select id="getTeacher" resultType="Teacher">
        select * from teacher
    </select>
</mapper>
  • 使用resultMap将二者连接
    <select id="getStudent" resultMap="ST_Map">
        select * from student
    </select>

    <resultMap id="ST_Map" type="Student">
        <association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
    </resultMap>
    
    <select id="getTeacher" resultType="Teacher">
        select * from teacher where id = #{id}
    </select>

看到这里应该有点懵,先不要着急往下看。

  • 测试类(平平无奇的测试类)
    @Test
    public void getStudentTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        List<Student> studentList = mapper.getStudent();
        for (Student student : studentList) {
            System.out.println(student);
        }
        sqlSession.close();
    }
  • 测试结果:

测试结果十分的让人满意,这样一来就满足了我们前面的需求,而且没有通过连表查询的方式

association

上面一系列操作,大家应该都能看得懂。让人不解的,最多只有这个 association标签了

  • 首先,我们使用 resultMap标签使实体类中的属性能够和数据库中的字段映射起来。

  • id、name自然是不需要我们对其进行映射的

  • 那唯一需要映射的就只有我们的 teacher属性了

  • 但这个属性它不是一个简单的字段,它是一个对象

  • resultMap中,简单的属性我们使用 result或者 id进行映射

  • 复杂的属性(比如对象、集合)我们使用 association和 collection来进行映射

association如何使用

<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
  • 既然要做映射,那 propertycolumn自然是必不可少
  • 如果只是这样简单映射,那我们也使用 result也能做到。但 teacher属性是一个对象
  • association标签中提供了 javaType,来方便我们指定 需要映射属性的对象类型
  • association标签还提供 select,它能够让我们指定一个select标签,也就是下面这个
    <select id="getTeacher" resultType="Teacher">
        select * from teacher where id = #{id}
    </select>
  • 到这里我们的 teacher属性,就与 tid字段完成了映射,且指定了对象类型,指定了嵌套的查询结果
  • 也就是说,我们的 tid会被作为 where id = #{id} 中的条件来先被执行
  • 返回的查询结果会被嵌套至 getStudent中的 teacher属性
  • 当我们执行 getStudent方法进行查询时,teacher会被嵌套为getTeacher的查询结果。

这一操作和我们 sql中的子查询很像,理解起来有一点困难,动手实现一下应该能更好的进行理解。

按照结果嵌套处理

除了上面这种将两条查询sql 互相嵌套,association还能够通过结果来进行嵌套处理

首先我们将 select标签体中的 sql进行一些修改,要写成一个连表查询的sql。

    <select id="getStudent" resultMap="ST_Map">
        SELECT s.id sid,s.name sname,t.id teacherId,t.name tname
        FROM student s,teacher t
        WHERE s.tid=t.id
    </select>

既然不再使用上一种方式,那相对的我们也需要对 resultMap标签进行一些修改

    <resultMap id="ST_Map" type="Student">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <association property="teacher" javaType="Teacher">
            <result property="name" column="tname"/>
            <result property="id" column="teacherId"/>
        </association>
    </resultMap>

可以看到,我们在 resultMap标签中对所有 sql中查询字段都进行了映射

而比较复杂的 teacher属性,我们仍然通过 association对其进行处理

但这一次我们不再为其指定一个 select标签来进行嵌套。而是在它标签体内,对 Teacher对象的属性和 sql中的teacher表字段进行映射。

都完成映射后,执行getStudent方法。

同样的,实现了我们想要的效果。以上就是association的两种使用方式

一对多

搭建一对多环境

Student实体类

public class Student {
    private int id;
    private String name;
    private int tid;
    // 以下省略 有参无参构造和getter、setter、toString方法

Teacher实体类

public class Teacher {
    private int id;
    private String name;
    private List<Teacher> teachers;
    // 同

抛出需求

我们想在查询老师的同时,将tid对应学生的数据查询出来。

使用sql语句也能很轻松的做到

一样的,我们通过MyBatis能不能实现?

尝试实现

在mapper接口中写一个查询老师的方法

public interface TeacherMapper {
    List<Teacher> getTeacher();
}

同步到mapper.xml中

<mapper namespace="com.molu.mapper.TeacherMapper">
    <select id="getTeacher" resultType="Teacher">
        select t.id,t.name,s.tid,s.id,s.name
        from teacher t,student s
        where t.id=s.tid
    </select>
</mapper>

测试类

    @Test
    public void getTeacherTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
        List<Teacher> teacherList = mapper.getTeacher();
        for (Teacher teacher : teacherList) {
            System.out.println(teacher);
        }
        sqlSession.close();
    }

测试结果

很明显,不能够将学生的信息查询出来,我们也不会通过一开始哪种方式,把学生的所有字段都写到老师的实体类中。

按照结果嵌套处理

在多对一中,我们也有使用过这种方式。实际上在一对多中实现也大致相似

首先我们对mapper.xml进行修改(和上文基本一样)

<mapper namespace="com.molu.mapper.TeacherMapper">
    <select id="getTeacher" resultMap="TS_Map">
        select t.id tid,t.name tname,s.id sid,s.name sname,s.tid stid
        from teacher t,student s
        where t.id=s.tid
    </select>
    <resultMap id="TS_Map" type="Teacher">
        <result property="id" column="tid"/>
        <result property="name" column="tname"/>
        <collection property="students" ofType="Student">
            <result property="id" column="sid"/>
            <result property="name" column="sname"/>
            <result property="tid" column="stid"/>
        </collection>
    </resultMap>
</mapper>

在上文我们有说复杂的属性(比如对象、集合)我们使用 association和 collection来进行映射

现在我们 Teacher中的 students属性就是一个复杂属性,是一个集合。

对象使用 association标签,那可想而知的集合我们会使用collection标签

Java对象我们使用JavaType来对其进行映射,而集合我们使用ofType

将这些进行修改后就和多对一中的几乎一样了。

测试类

    @Test
    public void getTeacherTest(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
        List<Teacher> teacherList = mapper.getTeacher();
        for (Teacher teacher : teacherList) {
            System.out.println(teacher);
        }
        sqlSession.close();
    }

测试结果:

有了多对一的基础,实现一对多也不会有太大难度。

接着我们看看让人比较痛苦的按照查询嵌套,也就是类似子查询的方式

按照查询嵌套处理

首先对mapper.xml文件进行修改

    <select id="getTeacher" resultMap="TS_Map">
        select t.id,t.name,s.id sid,s.name sname
        from teacher t,student s
        where t.id=s.tid
    </select>

    <resultMap id="TS_Map" type="Teacher">
        <collection property="students" column="sid" javaType="ArrayList" ofType="Student" select="getStudent"/>
    </resultMap>
    
    <select id="getStudent" resultType="Student">
        select * from student where id=#{sid}
    </select>
</mapper>

由于 teacher表中没有可以与 Student对象进行映射的字段我们只能使用连表查询的sql

collection标签中我们可以使用,JavaTypeofType来指定对象类型和泛型中的类型

这个写得可能有点乱,捋捋应该还是能够看得懂

以我的水平只能勉强写出这种代码,如果可以优化还望评论区简单指点一下

测试类不变

测试结果

也实现了我们上述的需求

那到这里MyBatis的简单注解使用、多对一、一对多也就告一段落了。关于MyBatis的文章在之后应该也会陆续更新,感谢你能看到这里 如果有帮到你的话 (‾◡◝)


放松一下眼睛

原图地址

画师主页