【精选必读】MyBatis关联查询及注解开发


在这里插入图片描述

MyBatis关联查询

MyBatis的关联查询分为一对一关联查询和一对多关联查询。

  • 查询对象时,将关联的另一个对象查询出来,就是一对一关联查询。
  • 查询对象时,将关联的另一个对象的集合查询出来,就是一对多关联查询。

例如有学生类和班级类:

一个学生对应一个班级,也就是学生类中有一个班级属性,这就是一对一关系。

一个班级对应多个学生,也就是班级类中有一个学生集合属性,这就是一对多关系。

实体类设计如下:

public class Student {
  private int sid;
  private String name;
  private int age;
  private String sex;
  private Classes classes;
  // 省略getter/setter/toString
}


public class Classes {
  private int cid;
  private String className;
  private List<Student> studentList;
  // 省略getter/setter/toString
}

数据库设计如下:

MyBatis一对一关联查询

查询学生时,将关联的一个班级对象查询出来,就是一对一关联查询。

创建持久层接口
public interface StudentMapper {
  List<Student> findAll();
}
创建映射文件
<resultMap id="studentMapper" type="com.mybatis.pojo.Student">
  <!-- 主键列 -->
  <id property="sid" column="sid"></id>
  <!-- 普通列 -->
  <result property="name" column="name"></result>
  <result property="age" column="age"></result>
  <result property="sex" column="sex"></result>
  <!-- 一对一对象列 property:属性名  column:关联列名 javaType:对象类型-->
  <association property="classes" column="classId" javaType="com.mybatis.pojo.Classes">
    <!-- 关联对象主键列 -->
    <id property="cid" column="cid"></id>
    <!-- 关联对象普通列 -->
    <result property="className" column="className"></result>
  </association>
</resultMap>

<!-- 多表查询,级联查询学生和其班级 -->
<select id="findAll" resultMap="studentMapper">
   select * from student left join classes on student.classId = classes.cid;
</select>
配置文件注册映射文件
<mappers>
  <package name="com.mybatis.mapper"/>
</mappers>

测试一对一关联查询
InputStream is = null;
SqlSession session = null;


@Before
public void before() throws IOException {
  is = Resources.getResourceAsStream("SqlMapConfig.xml");
  SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
  SqlSessionFactory factory = builder.build(is);
  session = factory.openSession();
}


@After
public void after() throws IOException {
  session.close();
  is.close();
}


@Test
public void testFindAllStudent(){
  StudentMapper studentMapper = session.getMapper(StudentMapper.class);
  List<Student> all = studentMapper.findAll();
  all.forEach(System.out::println);
}

MyBatis一对多关联查询

查询班级时,将关联的学生集合查询出来,就是一对多关联查询。

创建持久层接口
public interface ClassesMapper {
  List<Classes> findAll();
}
创建映射文件
<resultMap id="classesMapper" type="com.mybatis.pojo.Classes">
  <id property="cid" column="cid"></id>
  <result property="className" column="className"></result>
  <!-- 集合列  property:属性名  column:关联列名 ofType:集合的泛型 -->
  <collection property="studentList" column="classId" ofType="com.mybatis.pojo.Student">
    <id property="sid" column="sid"></id>
    <result property="name" column="name"></result>
    <result property="age" column="age"></result>
    <result property="sex" column="sex"></result>
  </collection>
</resultMap>


<!-- 多表查询,级联查询班级和它的学生 -->
<select id="findAll" resultMap="classesMapper">
   select * from classes left join student  on classes.cid = student.classId;
</select>
测试一对多关联查询
@Test
public void testFindAllClasses() {
  ClassesMapper classesMapper = session.getMapper(ClassesMapper.class);
  List<Classes> all = classesMapper.findAll();
  all.forEach(System.out::println);
}

MyBatis多对多关联查询

MyBatis多对多关联查询本质就是两个一对多关联查询。

例如有老师类和班级类:

一个老师对应多个班级,也就是老师类中有一个班级集合属性。

一个班级对应多个老师,也就是班级类中有一个老师集合属性。

实体类设计如下:

public class Teacher {
  private Integer tid;
  private String tname;
  private List<Classes> classes;
  // 省略getter/setter/toString
}


public class Classes {
  private Integer cid;
  private String className;
  private List<Student> studentList;
  private List<Teacher> teacherList;
  // 省略getter/setter/toString
}

在数据库设计中,需要建立中间表,双方与中间表均为一对多关系。

接下来测试查询老师时,将关联的班级集合查询出来。

创建持久层接口
public interface TeacherMapper {
  List<Teacher> findAll();
}

创建映射文件
<resultMap id="teacherMapper" type="com.mybatis.pojo.Teacher">
  <id column="tid" property="tid"></id>
  <result column="tname" property="tname"></result>
  <collection property="classes" column="tid" ofType="com.mybatis.pojo.Classes">
    <id column="cid" property="cid"></id>
    <result column="className" property="className"></result>
  </collection>
</resultMap>


<select id="findAll" resultMap="teacherMapper">
   select *
   from teacher
   left join classes_teacher
   on teacher.tid = classes_teacher.tid
   left join classes
   on classes_teacher.cid = classes.cid
</select>

测试多对多关联查询
@Test
public void testFindAllTeacher() {
  TeacherMapper teacherMapper = session.getMapper(TeacherMapper.class);
  List<Teacher> all = teacherMapper.findAll();
  all.forEach(System.out::println);
}

如果想查询班级时,将关联的老师集合查询出来,只需要修改班级映射文件的Sql语句和<resultMap>即可:

<resultMap id="classesMapper" type="com.mybatis.pojo.Classes">
  <id property="cid" column="cid"></id>
  <result property="className" column="className"></result>
  <!-- 集合列  property:属性名  column:关联列名 ofType:集合的泛型 -->
  <collection property="studentList" column="classId" ofType="com.mybatis.pojo.Student">
    <id property="sid" column="sid"></id>
    <result property="name" column="name"></result>
    <result property="age" column="age"></result>
    <result property="sex" column="sex"></result>
  </collection>
  <collection property="teacherList" column="cid" ofType="com.mybatis.pojo.Teacher">
    <id property="tid" column="tid"></id>
    <result property="tname" column="tname"></result>
  </collection>
</resultMap>


<select id="findAll" resultMap="classesMapper">
   select *
   from classes
   left join student
   on classes.cid = student.classId
   left join classes_teacher
   on classes.cid = classes_teacher.cid
   left join teacher
   on classes_teacher.tid = teacher.tid;
</select>

MyBatis分解式查询_一对多

在MyBatis多表查询中,使用连接查询时一个Sql语句就可以查询出所有的数据。如:

# 查询班级时关联查询出学生
select *
  from classes
  left join student
  on student.classId = classes.cid

也可以使用分解式查询,即将一个连接Sql语句分解为多条Sql语句,如:

# 查询班级时关联查询出学生
select * from classes;
select * from student where classId = 1;
select * from student where classId = 2; 

这种写法也叫N+1查询。

连接查询:

  • 优点:降低查询次数,从而提高查询效率。
  • 缺点:如果查询返回的结果集较多会消耗内存空间。

N+1查询:

  • 优点:结果集分步获取,节省内存空间。
  • 缺点:由于需要执行多次查询,相比连接查询效率低。

我们以查询班级时关联查询出学生为例,使用N+1查询:

创建每个查询语句的持久层方法
public interface ClassesMapper {
  // 查询所有班级
  List<Classes> findAll();
}


public interface StudentMapper {
  // 根据班级Id查询学生
  List<Student> findByClassId(int classId);
}
在映射文件中进行配置
<select id="findAll" resultType="com.mybatis.pojo.Classes">
   select * from classes
</select>


<select id="findByClassId" resultType="com.mybatis.pojo.Student" parameterType="int">
   select * from student where classId = ${classId}
</select>
修改主表映射文件中的查询方法
<!-- 自定义映射关系  -->
<resultMap id="MyClassesMapper" type="com.mybatis.pojo.Classes">
  <id property="cid" column="cid"></id>
  <result property="className" column="className"></result>
  <!-- select:从表查询调用的方法  column:调用方法时传入的参数字段   -->
  <collection property="studentList"
        ofType="com.mybatis.pojo.Student"        select="com.mybatis.mapper2.StudentMapper2.findByClassId"
        column="cid">
  </collection>
</resultMap>


<select id="findAll" resultMap="MyClassesMapper">
   select * from classes
</select>
测试查询方法
@Test
public void testFindAllClasses2(){
  ClassesMapper2 classesMapper2 = session.getMapper(ClassesMapper2.class);
  List<Classes> all = classesMapper2.findAll();
  all.forEach(System.out::println);
}

我们可以看到在控制台打印出了多条Sql语句

MyBatis分解式查询_一对一

查询学生时关联查询出班级也可以使用分解式查询,首先将查询语句分开:

select * from student;
select * from classes where cid = ?;
创建每个查询语句的持久层方法
public interface StudentMapper {
   // 查询所有学生
  List<Student> findAll();
}


public interface ClassesMapper {
  // 根据ID查询班级
  Classes findByCid(int cid);
}
在映射文件中进行配置
<select id="findAll" resultType="com.mybatis.pojo.Student">
   select *
   from student
</select>


<select id="findByCid" resultType="com.mybatis.pojo.Classes" parameterType="int">
   select * from classes where cid = ${cid}
</select>
修改主表映射文件中的查询方法
<resultMap id="MyStudentMapper" type="com.mybatis.pojo.Student">
  <id property="sid" column="sid"></id>
  <result property="name" column="name"></result>
  <result property="age" column="age"></result>
  <result property="sex" column="sex"></result>
  <association property="classes"
         javaType="com.mybatis.pojo.Classes"
         select="com.mybatis.mapper2.ClassesMapper2.findByCid"
         column="classId">
  </association>
</resultMap>


<select id="findAll" resultMap="MyStudentMapper">
   select *
   from student
</select>
测试查询方法
@Test
public void testFindAllStudent2(){
  StudentMapper2 studentMapper2 = session.getMapper(StudentMapper2.class);
  List<Student> all = studentMapper2.findAll();
  all.forEach(System.out::println);
}

MyBatis延迟加载

分解式查询又分为两种加载方式:

  • 立即加载:在查询主表时就执行所有的Sql语句。
  • 延迟加载:又叫懒加载,首先执行主表的查询语句,使用从表数据时才触发从表的查询语句。

延迟加载在获取关联数据时速度较慢,但可以节约资源,即用即取。

开启延迟加载
  • 设置所有的N+1查询都为延迟加载:

    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>
    
    
  • 设置某个方法为延迟加载:

    <association><collection>中添加fetchType属性设置加载方式。lazy:延迟加载;eager:立即加载。

测试延迟加载
@Test
public void testFindAllClasses2(){
  ClassesMapper2 classesMapper2 = session.getMapper(ClassesMapper2.class);
  List<Classes> all = classesMapper2.findAll();
  all.forEach(System.out::println);
  System.out.println("-------------------------");
  System.out.println(all.get(0).getStudentList());
}

由于打印对象时会调用对象的toString方法,toString方法默认会触发延迟加载的查询,所以我们无法测试出延迟加载的效果。

我们在配置文件设置lazyLoadTriggerMethods属性,该属性指定对象的什么方法触发延迟加载,设置为空字符串即可。

<settings>
  <setting name="lazyLoadTriggerMethods" value=""/>
</settings>

一般情况下,一对多查询使用延迟加载,一对一查询使用立即加载。

MyBatis注解开发

环境搭建

MyBatis可以使用注解替代映射文件。映射文件的作用就是定义Sql语句,可以在持久层接口上使用@Select/@Delete/@Insert/@Update定义Sql语句,这样就不需要使用映射文件了。

  1. 创建maven工程,引入依赖

  2. 创建mybatis核心配置文件SqlMapConfig.xml

  3. 将log4j.properties文件放入resources中,让控制台打印SQL语句。

  4. 创建实体类

  5. 创建持久层接口,并在接口方法上定义Sql语句

    public interface UserMapper {
      @Select("select * from user")
      List<User> findAll();
    }
    

    由于注解在方法上方,而方法中就有参数类型和返回值类型,所以使用注解开发不需要定义参数类型和返回值类型

  6. 在核心配置文件注册持久层接口,由于没有映射文件,所以只能采用注册接口或注册包的方法。

    <mappers>
      <package name="com.mybatis.mapper"/>
    </mappers>
    
  7. 测试方法

    InputStream is = null;
    SqlSession session = null;
    UserMapper userMapper = null;
    @Before
    public void before() throws IOException {
      is = Resources.getResourceAsStream("SqlMapConfig.xml");
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(is);
      session = factory.openSession();
      userMapper = session.getMapper(UserMapper.class);
    }
    
    @After
    public void after() throws IOException {
      session.close();
      is.close();
    }
    
    @Test
    public void testFindAll(){
      List<User> all = userMapper.findAll();
      all.forEach(System.out::println);
    }
    

增删改查

接下来写一套基于MyBatis注解的增删改查方法:

@SelectKey(keyColumn = "id", keyProperty = "id", resultType = int.class,before = false, statement = "SELECT LAST_INSERT_ID()")
@Insert("insert into user(username,sex,address) values(#{username},#{sex},#{address})")
void add(User user);


@Update("update user set username = #{username},sex=#{sex},address=#{address} where id = #{id}")
void update(User user);


@Delete("delete from user where id = #{id}")
void delete(int id);


@Select("select * from user where username like #{username}")
List<User> findByUsernameLike(String username);

动态Sql

MyBatis注解开发中有两种方式构建动态Sql:

使用脚本标签

将Sql嵌套在<script>内即可使用动态Sql标签:

// 根据任意条件查询
@Select("<script>" +
    "  select * from user\n" +
    "     <where>\n" +
    "       <if test=\"username != null and username.length() != 0\">\n" +
    "         username like #{username}\n" +
    "       </if>\n" +
    "       <if test=\"sex != null and sex.length() != 0\">\n" +
    "         and sex = #{sex}\n" +
    "       </if>\n" +
    "       <if test=\"address != null and address.length() != 0\">\n" +
    "         and address = #{address}\n" +
    "       </if>\n" +
    "     </where>" +
    "</script>")
List<User> findByCondition(User user);
在方法中构建动态Sql

在MyBatis中有@SelectProvider@UpdateProvider@DeleteProvider@InsertProvider注解。当使用这些注解时将不在注解中直接编写SQL,而是调用某个类的方法来生成SQL。

// 生成根据任意条件查询的Sql语句
public String findByConditionSql(User user){
  StringBuffer sb = new StringBuffer("select * from user where 1=1 ");
  if (user.getUsername() != null && user.getUsername().length() != 0){
    sb.append(" and username like #{username} ");
   }
  if (user.getSex() != null && user.getSex().length() != 0){
    sb.append(" and sex = #{sex} ");
   }
  if (user.getAddress() != null && user.getAddress().length() != 0){
    sb.append(" and address = #{address} ");
   }
  return sb.toString();
}

自定义映射关系

当POJO属性名与数据库列名不一致时,需要自定义实体类和结果集的映射关系,在MyBatis注解开发中,使用@Results定义并使用自定义映射,使用@ResultMap使用自定义映射,用法如下:

// 查询所有用户
@Results(id = "userDiyMapper" ,value = {
  @Result(id = true,property = "id",column = "id"),
  @Result(property = "username",column = "username1"),
  @Result(property = "sex",column = "sex1"),
  @Result(property = "address",column = "address1"),
})
@Select("select * from user")
List<User> findAll();


// 根据id查询
@ResultMap("userDiyMapper")
@Select("select * from user where id = #{id}")
User findById(int id);

二级缓存

MyBatis默认开启一级缓存,接下来我们学习如何在注解开发时使用二级缓存:

  1. POJO类实现Serializable接口。

  2. 在MyBatis配置文件添加如下设置:

    <settings>
      <setting name="cacheEnabled" value="true"/>
    </settings>
    
  3. 在持久层接口上方加注解@CacheNamespace(blocking=true),该接口的所有方法都支持二级缓存。

  4. 测试二级缓存

    @Test
    public void testCache() throws IOException {
      InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      SqlSessionFactory factory = builder.build(is);
      SqlSession session1 = factory.openSession();
      SqlSession session2 = factory.openSession();
    
    
      User user1 = session1.getMapper(UserMapper.class).findById(1);
      System.out.println(user1);
      System.out.println(user1.hashCode());
      session1.commit(); // 清空一次缓存,将数据存到二级缓存
      User user2 = session2.getMapper(UserMapper.class).findById(1);
      System.out.println(user2);
      System.out.println(user2.hashCode());
    }
    

一对一关联查询

在MyBatis的注解开发中对于多表查询只支持分解查询,不支持连接查询。

  1. 创建实体类

    public class Student {
      private int sid;
      private String name;
      private int age;
      private String sex;
      private Classes classes;
      // 省略getter/setter/toString
    }
    
    
    public class Classes {
      private int cid;
      private String className;
      private List<Student> students;
      // 省略getter/setter/toString
    }
    
  2. 创建分解后的查询方法

    public interface StudentMapper {
      @Select("select * from student")
      List<Student> findAll();
    }
    
    
    public interface ClassesMapper {
      // 根据id查询班级
      @Select("select * from classes where cid = #{cid}")
      Classes findByCid(Integer cid);
    }
    
  3. 主表的查询配置自定义映射关系

    @Select("select * from student")
    // 自定义映射关系
    @Results(id = "studentMapper",value = {
      @Result(id = true,property = "sid",column = "sid"),
      @Result(property = "name",column = "name"),
      @Result(property = "age",column = "age"),
      @Result(property = "sex",column = "sex"),
      /**
           * property:属性名
           * column:调用从表方法时传入的参数列
           * one:表示该属性是一个对象
           * select:调用的从表方法
           * fetchType:加载方式
           */
      @Result(property = "classes",column = "classId",
          one = @One(select = "com.mybatis.mapper.ClassesMapper.findByCid",
                fetchType = FetchType.EAGER))
    })
    List<Student> findAll();
    
  4. 测试

    @Test
    public void findAllStudent(){
      StudentMapper studentMapper = session.getMapper(StudentMapper.class);
      List<Student> all = studentMapper.findAll();
      all.forEach(System.out::println);
    }
    

一对多关联查询

  1. 创建分解后的查询方法

    public interface ClassesMapper {
      // 查询所有班级
      @Select("select * from classes")
      List<Classes> findAll();
    }
    
    
    public interface StudentMapper {
      // 根据班级id查询学生
      @Select("select * from student where classId = #{classId}")
      List<Student> findByClassId(int classId);
    }
    
    
  2. 主表的查询配置自定义映射关系

    // 查询所有班级
    @Select("select * from classes")
    @Results(id = "classMapper", value = {
      @Result(id = true, property = "cid", column = "cid"),
      @Result(property = "className", column = "className"),
      // many:表示该属性是一个集合
      @Result(property = "studentList", column = "cid",
          many = @Many(select = "com.mybatis.mapper.StudentMapper.findByClassId",
                 fetchType = FetchType.LAZY))
    })
    List<Classes> findAll();
    
    
  3. 测试

    @Test
    public void findAllClasses(){
      ClassesMapper classesMapper = session.getMapper(ClassesMapper.class);
      List<Classes> all = classesMapper.findAll();
      all.forEach(System.out::println);
    }
    
    

注解开发与映射文件开发的对比

MyBatis中更推荐使用映射文件开发,Spring、SpringBoot更推荐注解方式。具体使用要视项目情况而定。它们的优点对比如下:

映射文件:

  • 代码与Sql语句是解耦的,修改时只需修改配置文件,无需修改源码。
  • Sql语句集中,利于快速了解和维护项目。
  • 级联查询支持连接查询和分解查询两种方式,注解开发只支持分解查询。

注解:

  • 配置简单,开发效率高。
  • 类型安全,在编译期即可进行校验,不用等到运行时才发现错误。
posted @ 2023-11-26 17:00  Gjq-  阅读(91)  评论(0编辑  收藏  举报  来源