Mybatis框架(动态SQL及Mybatis缓存)

本文详细介绍了如何在Mybatis中使用动态SQL拼接进行条件搜索,并结合缓存、级联查询和延迟加载优化性能。涵盖了一对一、一对多关联查询,以及如何配置全局和局部懒加载策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Mybatis

  • ORM:对象关系映射。每张表要和一个JavaBean对应

    • 关系->对象:指从表里查询数据,把结果封装成对应的JavaBean对象
    • 对象->关系:指把JavaBean里的数据,保存到表里对应的字段上
    • Hibernate是一个全ORM框架
    • Mybatis是一个半ORM框架:只实现了 关系->对象
  • 搭建Mybatis开发环境

    1. 创建项目,导入jar包,准备JavaBean

    2. 使用Mybatis实现功能

      1. 先创建dao接口(映射器Mapper):不需要实现类

      2. 再给每个接口创建一个xml文件(映射配置文件),主要配置每个方法的SQL语句:

        映射文件和映射器接口,要求:同名同位置

      3. 还需要创建一个全局配置文件:主要配置数据源、映射器

        全局配置文件名称随意,位置建议放到src下

      4. 可以再配置一个日志配置文件

    3. 功能测试

      //1. 加载全局配置文件
      InputStream is = Resources.getResourceAsStream("全局配置文件");
      //2. 得到一个SqlSession对象
      SqlSessionFactory factory = new SqlSesionFactoryBuilder().build(is);
      SqlSession session = factory.openSession();
      //3. 得到dao接口的代理对象
      UserDao userDao = session.getMapper(UserDao.class);
      
      //4. 释放资源
      session.close();
      is.close();
      
  • 代理方式的CURD功能实现

    1. 在dao接口里增加方法
    2. 在接口配置文件里配置SQL语句
    <select id="方法名" parameterType="参数类型" resultType="结果集封装的类型">
        select语句
    </select>
    <insert id="方法名" parameterType="参数类型">
    	<selectKey resultType="主键值类型" keyProperty="主键值存储到哪个属性" order="AFTER">
        	select last_insert_id()
        </selectKey>
        insert语句
    </insert>
    <update id="方法名" parameterType="参数类型">
    	update语句
    </update>
    <delete id="方法名" parameterType="参数类型">
    	delete语句
    </delete>
    
  • 参数说明

    • 单参数

      • 如果参数是简单类型,SQL语句里取参数值:#{随便}
      • 如果参数是JavaBean,SQL语句里取参数值:#{JavaBean的属性名}
      • 如果参数是复杂JavaBean,SQL语句里取参数值:#{JavaBean属性名.属性名}
    • 多参数

      • SQL取值方案一:#{arg0}, #{arg1}, ....
      • SQL取值方案二:#{param1}, #{param2}, ...
      • 给参数命名,根据名称取参数值
      List<User> search(@Param("username")String username, @Param("sex")String sex);
      
      <select>
      	select * from user where username like "%"#{username}"%" and sex = #{sex}
      </select>
      
  • 结果集封装

    • resultType:自动映射。要求JavaBean属性名 和 表字段名/字段别名 相同
    • resultMap:手动映射。手动配置一下 JavaBean属性名 和 表字段名的对应关系
    <select id="" resultMap="map">
    </select>
    <resultMap id="map" type="JavaBean的全限定类名/别名">
    	<id property="JavaBean属性名" column="主键字段名"/>
        <result property="JavaBean属性名" column="表字段名"/>
    </resultMap>
    
  • 全局配置文件

    <typeAliases>
    	<package name="com.is.fwis.domain"/>
    </typeAliases>
    <environments default="">
    	....
    </environments>
    <mappers>
    	<package name="com.is.fwis.dao"/>
    </mappers>
    

一、动态sql拼接

目标

  • 能够使用mybatis的标签实现动态SQL拼接

分析

​ 我们在前边的学习过程中,使用的SQL语句都非常简单。而在实际业务开发中,我们的SQL语句通常是动态拼接而成的,比如:条件搜索功能的SQL语句。

# 提供了一个功能:用户可以在页面上根据username、sex、address进行搜索
# 用户输入的搜索条件:可以是一个条件,也可能是两个、三个

# 只输入一个条件:姓名是"王"
SELECT * FROM USER WHERE username LIKE '%王%'
# 只输入一个条件:性别是“男”
SELECT * FROM USER WHERE sex = '男'
# 输入两个条件:姓名“王”,性别“男”
SELECT * FROM USER WHERE username LIKE '%王%' AND sex = '男'
# 输入三个条件:姓名“王”,性别“男”,地址“北京”
SELECT * FROM USER WHERE username LIKE '%王%' AND sex = '男' AND address LIKE '%北京%';

在Mybatis中,SQL语句是写在映射配置的XML文件中的。Mybatis提供了一些XML的标签,用来实现动态SQL的拼接。

​ 常用的标签有:

  • <if></if>:用来进行判断,相当于Java里的if判断
  • <where></where>:通常和if配合,用来代替SQL语句中的where 1=1
  • <foreach></foreach>:用来遍历一个集合,把集合里的内容拼接到SQL语句中。例如拼接:in (value1, value2, ...)
  • <sql></sql>:用于定义sql片段,达到重复使用的目的

讲解

1. 准备Mybatis环境
  1. 创建java项目,导入jar包;准备JavaBean
  2. 创建映射器接口UserDao
  3. 创建映射配置文件UserDao.xml
  4. 创建全局配置文件SqlMapConfig.xml
  5. 创建日志配置文件log4j.properties
2. <if>标签:
语法介绍
<if test="判断条件,使用OGNL表达式进行判断">
	SQL语句内容, 如果判断为true,这里的SQL语句就会进行拼接
</if>
使用示例
  • 根据用户的名称和性别搜索用户信息。把搜索条件放到User对象里,传递给SQL语句
  1. 映射器接口UserDao上加方法
package com.is.fwis.dao;

import com.is.fwis.domain.User;

import java.util.List;

/**
 * @author lcz
 * @date 2021/12/7
 */
public interface UserDao {
    /**
     * 根据username和sex搜索用户
     * @param user 封装了搜索条件的User对象
     * @return 搜索的结果
     */
    List<User> search1(User user);
}
  1. 映射文件UserDao.xml里配置statement
<?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.is.fwis.dao.UserDao">

    <!--
    if标签:用于条件判断
        语法:<if test="用OGNL表达式判断"> 如果判断为true,这里的内容会拼接上去 </if>
        注意:标签里写OGNL表达式,不要再加#{}、${}
        常用的OGNL表达式:
            比较:>, <, >=, <=, ==, != 或者 gt, lt, gte, lte, eq, neq
            逻辑:&&,||,! 或者 and, or, not
            调用方法:username.length(),  list.size()
    -->
    <select id="search1" resultType="User">
        select * from user where 1=1
        <if test="username != null and username.length()>0">
            and username like "%"#{username}"%"
        </if>
        <if test="sex != null and sex.length()>0">
            and sex = #{sex}
        </if>
    </select>
</mapper>
  1. 功能测试,在测试类里加测试方法
package com.is.fwis;

import com.is.fwis.dao.UserDao;
import com.is.fwis.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * @author lcz
 * @date 2021/12/07
 */
public class SqlTest {

    private UserDao userDao;
    private SqlSession session;
    private InputStream is;

    /**
     * 要求:根据username和sex搜索用户
     *      搜索条件放到user对象里
     */
    @Test
    public void testSearch(){
        User user = new User();
        // user.setUsername("王");
        // user.setSex("男");

        List<User> userList = userDao.search1(user);
        userList.forEach(System.out::println);
    }


    @Before
    public void init() throws IOException {
        //1. 读取全局配置文件
        is = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2. 得到一个SqlSession对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        session = factory.openSession();
        userDao = session.getMapper(UserDao.class);
    }

    @After
    public void destroy() throws IOException {
        session.close();
        is.close();
    }
}
3. <where>标签
语法介绍

在刚刚的练习的SQL语句中,我们写了where 1=1。如果不写的话,SQL语句会出现语法错误。Mybatis提供了一种代替where 1=1的技术:<where></where>标签。

代码示例

​ 把上一章节的实现代码进行优化,使用<where></where>标签代替where 1=1

  1. 映射器UserDao的search1方法:已有,不用修改
/**
 * 根据username和sex搜索用户
 * @param user 封装了搜索条件的User对象
 * @return 搜索的结果
 */
List<User> search1(User user);
  1. 在映射文件UserDao.xml里修改SQL语句
<!--
    where标签:让Mybatis帮我们生成一个where关键字
        Mybatis会智能判断:
            如果一个条件都没有,就不生成where关键字
            如果有条件,会判断是否有多余的and关键字,把多余的and去掉
        注意:建议把所有的where条件都放到where标签里边
    -->
<select id="search1" resultType="User">
    select * from user
    <where>
        <if test="username != null and username.length()>0">
            and username like "%"#{username}"%"
        </if>
        <if test="sex != null and sex.length()>0">
            and sex = #{sex}
        </if>
    </where>
</select>
  1. 在测试类里进行功能测试:测试方法不需要修改
@Test
public void testSearch(){
    User user = new User();
    // user.setUsername("王");
    // user.setSex("男");

    List<User> userList = userDao.search1(user);
    userList.forEach(System.out::println);
}
4. <foreach>标签
语法介绍

​ foreach标签,通常用于循环遍历一个集合,把集合的内容拼接到SQL语句中。例如,我们要根据多个id查询用户信息,SQL语句:

select * from user where id = 1 or id = 2 or id = 3;
select * from user where id in (1, 2, 3);

​ 假如我们传参了id的集合,那么在映射文件中,如何遍历集合拼接SQL语句呢?可以使用foreach标签实现。

<!--
foreach标签:
	属性:
		collection:被循环遍历的对象,使用OGNL表达式获取,注意不要加#{}
		open:循环之前,拼接的SQL语句的开始部分
		item:定义变量名,代表被循环遍历中每个元素,生成的变量名
		separator:分隔符
		close:循环之后,拼接SQL语句的结束部分
	标签体:
		使用#{OGNL}表达式,获取到被循环遍历对象中的每个元素
-->
<foreach collection="" open="id in(" item="id" separator="," close=")">
    #{id}
</foreach>
使用示例
  1. 有搜索条件类QueryVO如下:
package com.is.fwis.domain;

public class QueryVO {
    private Integer[] ids;

    public Integer[] getIds() {
        return ids;
    }

    public void setIds(Integer[] ids) {
        this.ids = ids;
    }
}

  1. 在映射器UserDao里加方法
/**
     * QueryVO里有一个Integer[] ids
     * 要求:根据ids查询对应的用户列表
     */
List<User> search2(QueryVO vo);
  1. 在映射文件UserDao.xml里配置statement
    <!--
    foreach标签:用于循环遍历
        collection:被循环的集合/数组
        item:定义一个变量
        separator:定义拼接时的分隔符
        open:拼接字符串时的开始部分
        close:拼接字符串时的结束部分

        相当于 for(Integer id: ids){}
        select * from user where id in(41, 42, 45)
    -->
    <select id="search2" resultType="User">
        <!--select * from user where id in(41, 42, 45)-->
        select * from user where
        <foreach collection="ids" open="id in(" item="id" separator="," close=")">
            #{id}
        </foreach>
    </select>
  1. 功能测试
    @Test
    public void testSearch2(){
        QueryVO vo = new QueryVO();
        vo.setIds(new Integer[]{41,42,43,44,45});
        List<User> userList = userDao.search2(vo);
        userList.forEach(System.out::println);
    }
5. <sql>标签

在映射文件中,我们发现有很多SQL片段是重复的,比如:select * from user。Mybatis提供了一个<sql>标签,把重复的SQL片段抽取出来,可以重复使用。

语法介绍

在映射文件中定义SQL片段:

<sql id="唯一标识">sql语句片段</sql>

在映射文件中引用SQL片段:

<include refid="sql片段的id"></include>
使用示例

在查询用户的SQL中,需要重复编写:select * from user。把这部分SQL提取成SQL片段以重复使用

  • 要求:QueryVO里有ids,user对象。根据条件进行搜索
  1. 修改QueryVO,增加成员变量user
package com.is.fwis.domain;

/**
 * @author lcz
 * @date 2021/12/07
 */
public class QueryVO {
    private Integer[] ids;
    private User user;

    //get/set方法……
}
  1. 在映射器UserDao里加方法
    /**
     * 动态SQL拼接的综合应用:if、where、foreach
     * 要求:QueryVo里有ids、username、sex值,根据这些值进行搜索
     */
    List<User> search3(QueryVO vo);
  1. 在映射文件UserDao.xml里配置statement
<select id="search3" resultType="User">
    <!--select * from user-->
    <include refid="selUser"/>
    <where>
        <if test="ids != null and ids.length > 0">
            <foreach collection="ids" open="and id in(" item="id" separator="," close=")">
                #{id}
            </foreach>
        </if>
        <!--<if test="user != null">
                <if test="user.username != null and user.username.length() > 0">
                    and username like "%"#{user.username}"%"
                </if>
                <if test="user.sex != null and user.sex.length() > 0">
                    and sex = #{user.sex}
                </if>
            </if>-->
        <include refid="userCondition"/>
    </where>
</select>

<!--
    sql标签:用于定义一个sql片段
    include标签:什么时候要引用某个SQL片段,就使用include标签
    注意:引入SQL片段之后,最终的SQL语句必须要完全符合语法
    -->
<sql id="selUser">select * from user</sql>
<sql id="userCondition">
    <if test="user != null">
        <if test="user.username != null and user.username.length() > 0">
            and username like "%"#{user.username}"%"
        </if>
        <if test="user.sex != null and user.sex.length() > 0">
            and sex = #{user.sex}
        </if>
    </if>
</sql>
  1. 在测试类里加测试方法
    @Test
    public void testSearch3(){
        QueryVO vo = new QueryVO();
        vo.setIds(new Integer[]{41,42,43,44,45});

        // User user = new User();
        // user.setUsername("王");
        // user.setSex("男");
        // vo.setUser(user);

        List<User> userList = userDao.search3(vo);
        userList.forEach(System.out::println);
    }

小结

<select>
	select * from user 
    <where>
        <if test="ids != null and ids.length > 0">
        	<foreach collection="ids" item="id" separator="," open="and id in(" close=")">
            	#{id}
            </foreach>
        </if>
        <if test="user != null">
            <if test="username != null and username.length()>0">
                and username like "%"#{username}"%"
            </if>
            <if test="sex != null and sex.length()>0">
                and sex = #{sex}
            </if>
        </if>
    </where>
</select>


<select>
	select * from user
    <include refid="condition"/>
</select>
<sql id="condition">
	<where>
        <if test="ids != null and ids.length > 0">
        	<foreach collection="ids" item="id" separator="," open="and id in(" close=")">
            	#{id}
            </foreach>
        </if>
        <if test="user != null">
            <if test="username != null and username.length()>0">
                and username like "%"#{username}"%"
            </if>
            <if test="sex != null and sex.length()>0">
                and sex = #{sex}
            </if>
        </if>
    </where>
</sql>

拓展:<set>标签

语法介绍

set标签用于代替update语句 的set关键字,实现动态拼接update语句,它可以用来动态包含需要更新的列,帮我们去掉多余的逗号。

使用示例
  • 在映射器接口UserDao里添加方法
void edit(User user);
  • 在映射文件UserDao.xml里配置SQL语句
<update id="edit" parameterType="com.is.fwis.domain.User">
	update user
    <set>
    	<if test="username!=null and username.length()>0">username = #{username},</if>
        <if test="sex != null and sex.length()>0">sex = #{sex},</if>
        <if test="birthday != null">birthday = #{birthday},</if>
    </set>
    where id = #{id}
</update>
  • 功能测试
@Test
public void testEdit(){
    User user = new User();
    user.setId(51);
    user.setUsername("张三");
    
    userDao.edit(user);
    session.commit();
}

二、Mybatis的缓存

缓存:高速的临时数据存储

1. 准备环境

略。按步骤准备好User和UserDao、UserDao.xml、SqlMapConfig.xml、log4j.properties

2. 一级缓存

目标
  • 了解Mybatis的一级缓存
讲解
一级缓存介绍
  • 一级缓存,是SqlSession对象提供的缓存
  • 执行一次查询之后,查询的结果(JavaBean对象)会被缓存到SqlSession中。
  • 再次查询同样的数据,Mybatis会优先从缓存中查找;如果找到了,就不再查询数据库。
一级缓存的清除
  • 当调用了SqlSession对象的修改、添加、删除、commit()、close()、clearCache()等方法时,一级缓存会被清空。
效果演示
package com.is.fwis;

import com.is.fwis.dao.UserDao;
import com.is.fwis.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

/**
 * Mybatis本身支持缓存:
 *
 *  一级缓存:缓存的有效范围较小,在同一个SqlSession里有效。
 *      缓存本质是一个HashMap,而这个Map在SqlSession对象内部
 *      使用一个SqlSession,第一次查询id为41的用户:没有缓存,要从数据库里查询;查询完成,结果会被缓存起来
 *      使用同一SqlSession,再一次查询id为41的用户:有缓存了,就从缓存里直接得到结果,不会再执行SQL语句了
 *
 *  二级缓存:缓存的有效范围较大,多个SqlSession可以共享二级缓存的数据
 *
 *
 * @author lcz
 * @date 2021/12/07
 */
public class CacheTest {

    private UserDao userDao;
    private SqlSession session;
    private InputStream is;
    private SqlSessionFactory factory;

    /**
     *  第一次查询:
     *      使用一个SqlSession,查询id为41的用户,得到:user1
     *  第二次查询:
     *      使用同一SqlSession,查询id为41的用户,得到:user2
     *  验证:
     *      1. 如果一级缓存有效,SQL语句会执行几次?一次
     *      2. 如果一级缓存有效,两次得到的user对象是否是同一个?是
     *
     *  当我们执行了增、删、改、关闭session、提交事务、clearCache方法时,一级缓存会被清除掉
     */
    @Test
    public void testLevel1Cache(){
        User user1 = userDao.findById(41);

        //如果在这里使用SqlSession执行了增、删、改、关闭session、提交事务、clearCache方法时,Mybatis会把缓存清除掉
        session.clearCache();

        User user2 = userDao.findById(41);

        System.out.println(user1==user2);
    }

    @Before
    public void init() throws IOException {
        //1. 读取全局配置文件
        is = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2. 得到一个SqlSession对象
        factory = new SqlSessionFactoryBuilder().build(is);
        session = factory.openSession();
        userDao = session.getMapper(UserDao.class);
    }

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

3. 二级缓存

目标
  • 了解Mybatis二级缓存的效果
讲解
3.1 什么是二级缓存
  • 二级缓存是Mapper级别的缓存(映射器级别的缓存)
  • 同样的映射器Mapper共享二级缓存。UserDao
  • 注意:
    • 二级缓存,缓存的是序列化之后的数据;
    • 当从缓存里取数据时,要进行反序列化还原成JavaBean对象
    • 要求JavaBean必须实现序列化接口Serializable
    • 二级缓存需要手动开启

在这里插入图片描述

3.2 开启二级缓存
  1. 修改全局配置文件,开启二级缓存
<settings>
    <!--开启全局的二级缓存-->
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. 哪个映射器要使用二级缓存,就在哪个映射文件里开启二级缓存支持[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NV6JJLsi-1638775682178)(img/image-20210503150151563.png)]

  2. 哪个方法要使用二级缓存,就在方法的配置statement上加useCache="true"[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcbuZD7E-1638775682180)(img/image-20210503150236695.png)]

3.3 效果演示
    /**
     * 二级缓存:全局缓存。即使把session关闭了,重新得到一个session查询相同数据时,二级缓存依然有效
     *         二级缓存默认是关闭的,需要手动开启
     *
     * 开启二级缓存
     *      1. 在全局配置文件里开启二级缓存
     *      2. 哪个映射器需要用二级缓存,就在映射配置文件里开启
     *      3. 哪个方法需要用二级缓存,就在方法的配置上,增加属性useCache="true"
     * 注意:二级缓存,存储的是序列化之后的字节;获取缓存数据时会反序列化恢复成JavaBean对象
     *      给User类实现Serializable接口
     *
     * 第一次:
     *      使用factory得到一个SqlSession:session1
     *      使用session1得到UserDao代理对象:userDao1
     *      使用userDao1查询id为41的用户,得到:user1
     *      关闭session1
     *
     * 第二次:
     *      使用factory得到一个SqlSession:session2
     *      使用session2得到UserDao代理对象:userDao2
     *      使用userDao2查询id为41的用户,得到:user2
     *      关闭session2
     *
     * 验证:
     *  如果二级缓存有效,SQL语句会执行几次?一次
     *  如果二级缓存有效,两次得到的user对象地址是否相同?不同
     */
    @Test
    public void testLevel2Cache(){
        //第一次:
        SqlSession session1 = factory.openSession();
        UserDao userDao1 = session1.getMapper(UserDao.class);
        User user1 = userDao1.findById(41);
        session1.close();


        //第二次:
        SqlSession session2 = factory.openSession();
        UserDao userDao2 = session2.getMapper(UserDao.class);
        User user2 = userDao2.findById(41);
        session2.close();

        System.out.println(user1==user2);
    }

三、嵌套查询(级联查询)

如果要查询一条数据,有很多关联的其它数据,该如何实现呢?可以使用Mybatis的查询嵌套方式实现需求

如下图所示:

  • 要查询一条用户数据,封装成User
  • 但是User里还要有:
    • 当前User关联的订单列表
    • 当前User关联的帐号列表
    • 当前User关联的角色列表
    • 当前User关联的收货地址列表
    • 当前User关联的用户详情
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H0d9bx9F-1638775682181)(img/image-20210907112841340.png)]

1. 准备开发环境

  1. 创建项目,导入jar包,创建JavaBean
  2. 创建映射器接口UserDao和AccountDao
  3. 创建映射文件UserDao.xml 和AccountDao.xml
  4. 创建Mybatis的全局配置文件。 注意,要配置好别名和映射器
  5. 准备好log4j日志配置文件
  6. 编写单元测试类备用

2. 关联一项数据

目标
  • 查询所有帐号,及关联的用户。一个帐号Account只关联一个用户User
分析
  1. 修改实体类Account
  2. 在映射器里加方法
  3. 在映射文件配置statement:
    • 编写SQL语句,只查询所有的帐号select * from account
  • 使用resultMap封装结果集,使用association封装关联的那个User
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cAUDKiQl-1638775682182)(img/image-20210907112918786.png)]
实现
1. 修改实体类Account
public class Account {
    private Integer id;
    private Integer uid;
    private Double money;

    private User user;
 
    //get/set...
}
2. 在AccountDao里增加方法
    /**
     * 查询所有帐号,及每个帐号关联的一个User。一个Account关联一个User
     */
    List<Account> queryAllAccount();
3. 在AccountDao.xml里配置statement
<select id="queryAllAccount" resultMap="accountMap">
    select * from account
</select>
<resultMap id="accountMap" type="Account">
    <id property="id" column="id"/>
    <result property="uid" column="uid"/>
    <result property="money" column="money"/>

    <!--
        一个Account关联一个User:使用association标签
            property:属性名
            javaType:这个属性的类型,写全限定类名或者别名
            select:让Mybatis调用哪个方法,可以得到这个Account关联的User对象
                需要一个方法:查询某帐号关联的用户=>根据帐号里的uid查询对应的User
            column:让Mybatis调用方法时,方法需要的参数,从当前结果集哪个字段里取
        -->
    <association property="user" javaType="User"
                 select="com.is.fwis.dao.UserDao.findById"
                 column="uid"/>
</resultMap>
4. 编写“查询一个用户”的方法
1. 在UserDao里增加方法
    /**
     * 根据uid查询对应的User
     */
    User findById(Integer uid);
2. 在UserDao.xml配置statement
<select id="findById" resultType="User">
    select * from user where id = #{uid}
</select>
5. 测试
package com.is.fwis;

import com.is.fwis.dao.AccountDao;
import com.is.fwis.dao.UserDao;
import com.is.fwis.domain.Account;
import com.is.fwis.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * 嵌套查询(级联查询)测试
 * @author lcz
 * @date 2021/12/07
 */
public class NestedQueryTest {
    private InputStream is;
    private SqlSession session;
    private UserDao userDao;
    private AccountDao accountDao;

    /**
     * 查询帐号,及关联的用户。一个帐号关联一个用户
     */
    @Test
    public void testQueryAllAccount(){
        List<Account> accounts = accountDao.queryAllAccount();
        accounts.forEach(System.out::println);
    }


    @Before
    public void init() throws IOException {
        //1. 读取全局配置文件
        is = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2. 得到一个SqlSession对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        session = factory.openSession();
        userDao = session.getMapper(UserDao.class);
        accountDao = session.getMapper(AccountDao.class);
    }

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

3. 关联一个集合

目标
  • 查询用户信息,及其关联的帐号信息集合。
分析
public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    /**因为一个用户有多个帐号,所以这里要有Account的集合*/
    private List<Account> accounts;
 	
    //get/set...
}
  1. 在UserDao里增加方法,查询所有用户。执行的语句select * from user
  2. 在UserDao.xml里配置statement:
    • 要使用resultMap封装结果集
    • 当封装User里accounts时,使用<collection property="accounts" ofType="Account" select="" column=""/>
实现
1. 在UserDao里增加方法
    /**
     * 查询所有用户,及关联的帐号集合。一个User关联多个Account
     */
    List<User> queryAllUser();
2. 在UserDao.xml里配置statement
<select id="queryAllUser" resultMap="userMap">
    select * from user
</select>
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="birthday" column="birthday"/>
    <result property="sex" column="sex"/>
    <result property="address" column="address"/>

    <!--
        关联一个account集合:使用collection标签
            property:属性名称
            ofType:集合里的每个成员的类型,写全限定类名或别名
            select:让Mybatis调用哪个方法,可以得到这个集合
                需要有一个方法:查询当前这个用户的帐号集合
            column:让Mybatis调用方法时,方法的参数从哪个字段取
        -->
    <collection property="accounts" ofType="Account"
                select="com.is.fwis.dao.AccountDao.findAccountsByUid"
                column="id"/>
</resultMap>
3. 准备根据用户id查询帐号集合的功能
3.1 在AccountDao里增加方法
    /**
     * 查询某一用户的帐号集合
     * @param uid 用户id
     * @return uid对应的帐号集合
     */
    List<Account> findAccountsByUid(Integer uid);
3.2 在AccountDao.xml里配置statement
<select id="findAccountsByUid" resultType="Account">
    SELECT * FROM account WHERE uid = #{uid}
</select>
4. 测试
/**
 * 查询用户,及关联的帐号。一个用户关联多个帐号
 */
@Test
public void testQueryAllUser(){
    List<User> userList = userDao.queryAllUser();
    userList.forEach(System.out::println);
}

四、延迟加载

多表关联查询时,比如查询用户信息,及其关联的帐号信息,在查询用户时就直接把帐号信息也一并查询出来了。但是在实际开发中,并不是每次都需要立即使用帐号信息,这时候,就可以使用延迟加载策略了。

​ 需要在查询嵌套方式的基础上,才能开启懒加载。

1. 什么是延迟加载
立即加载
  • 不管数据是否需要使用,只要调用了方法,就立即发起查询。
  • 比如:查询帐号,得到关联的用户;查询用户,得到关联的帐号
延迟加载
  • 延迟加载,也叫按需加载,或者叫懒加载

    • 只有当真正使用到数据的时候,才发起查询。不使用不发起查询
    • 比如:查询用户信息,不使用accounts的时候,不查询帐号的数据;只有当使用了用户的accounts,Mybatis再发起查询帐号的信息
  • 先从单表查询,需要使用关联数据时,才进行关联数据的查询。单表查询的速度要比多表关联查询速度快,性能高;

  • 用不上的数据,暂时不查询,内存占用小

延迟加载的使用场景
  • 一对一(多对一),通常不使用延迟加载(建议)。比如:查询帐号,关联加载用户信息
  • 一对多(多对多),通常使用延迟加载(建议)。比如:查询用户,关联加载帐号信息
2. 实现延迟加载

只需要在查询嵌套方式的基础上,开启懒加载即可。 开启懒加载方式:

方式一:在全局配置文件中增加配置,开启全局的懒加载

<settings>
    <!--开启 懒加载的全局开关-->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!--关闭 积极加载-->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

方式二:局部配置懒加载

  • association标签上、或collection标签上增加属性fetchType,把当前关联加载设置为懒加载
  • 局部配置,会覆盖掉全局配置
<association fetchType="lazy"></association>
<collection fetchType="lazy"></collection>

注意:

  • 在配置了延迟加载策略后,即使没有调用关联对象的任何方法,当你调用当前对象的
    equals、clone、hashCode、toString方法时也会触发关联对象的查询。
  • 在配置文件中可以使用lazyLoadTriggerMethods配置项覆盖掉mybatis的默认行为。
<!-- 指定对象的哪些方法触发加载 -->
<setting name="lazyLoadTriggerMethods" value="getUid,toString"/>

总结

  • 懒加载:延迟加载,按需加载。当用不到关联的数据时,就不查询;只要用到时,才会查询
  • 开启懒加载:
    • 全局开启
    • 局部开启:在association标签或者collection标签上加属性 fetchType="lazy"

五、多表关联查询

1. 搭环境

搭建Mybatis开发环境:略

针对user表和account表进行关联查询。两张表的关系是:一个user对多个account

分别准备好两张表对应的实体类UserAccount

2. 一对一(多对一)关联查询

目标
  • 需求:查询所有帐户表信息,及其关联的用户信息
分析
  1. 编写多表查询的SQL语句。

    注意:查询结果集里一定不能有重名列;如果有重名列,就起别名,保证所有列名不重复

    #查询所有帐户表信息,及其关联的用户信息
    SELECT a.id aid, a.uid, a.money, u.* FROM account a LEFT JOIN USER u ON a.uid = u.id
    
  2. 调整的JavaBean实体类

    要查询帐号,就封装到Account对象里

    每个帐号关联一个用户数据:在Account里加一个成员变量private User user

  3. 使用Mybatis执行SQL,使用resultMap封装结果

    当封装关联的那个User时,使用association标签

实现
1) 修改Account类

Account中要有User的引用

public class Account {
    private Integer id;
    private Integer uid;
    private Double money;

    private User user;
 
    //get/set...
}
2) 在映射器接口AccountDao中增加方法
/**
 * @author lcz
 * @date 2021/12/07
 */
public interface AccountDao {
    /**
     * 查询所有帐号,及每个帐号关联的一个User。一个Account关联一个User
     */
    List<Account> queryAllAccount();

}
3) 在映射文件AccountDao.xml中增加statement
  • 使用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.is.fwis.dao.AccountDao">
    <select id="queryAllAccount" resultMap="accountMap">
        SELECT a.id aid, a.uid, a.money, u.* FROM account a LEFT JOIN USER u ON a.uid = u.id
    </select>
    <resultMap id="accountMap" type="Account">
        <id property="id" column="aid"/>
        <result property="uid" column="uid"/>
        <result property="money" column="money"/>

        <association property="user" javaType="User">
            <id property="id" column="id"/>
            <result property="username" column="username"/>
            <result property="birthday" column="birthday"/>
            <result property="sex" column="sex"/>
            <result property="address" column="address"/>
        </association>
    </resultMap>
</mapper>
4) 在单元测试类中编写测试代码
package com.is.fwis;

import com.is.fwis.dao.AccountDao;
import com.is.fwis.dao.UserDao;
import com.is.fwis.domain.Account;
import com.is.fwis.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * 多表查询测试
 * @author lcz
 * @date 2021/12/07
 */
public class DuobiaoQueryTest {
    private InputStream is;
    private SqlSession session;
    private UserDao userDao;
    private AccountDao accountDao;

    @Test
    public void testQueryAllAccount(){
        List<Account> accounts = accountDao.queryAllAccount();
        accounts.forEach(System.out::println);
    }

    @Before
    public void init() throws IOException {
        //1. 读取全局配置文件
        is = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2. 得到一个SqlSession对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        session = factory.openSession();
        userDao = session.getMapper(UserDao.class);
        accountDao = session.getMapper(AccountDao.class);
    }

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

3. 一对多(多对多)关联查询

  • 查询所有用户(user)信息,以及每个用户拥有的所有帐号(account)信息
分析
  1. 编写多表查询SQL语句

    注意:查询结果集里不能有重名列。如果有,就起别名,保证所有列名不重复

    #查询所有用户(user)信息,以及每个用户拥有的所有帐号(account)信息
    SELECT u.*, a.id aid, a.uid, a.money FROM USER u LEFT JOIN account a ON u.id = a.uid
    
  2. 改造JavaBean

    把查询结果集里user表数据,封装到User对象里

    一个用户关联多个帐号,所以在User里应该有一个private List<Account> accounts;

  3. 执行SQL语句,用resultMap封装结果;

    当封装到其中关联的集合List<Account>时,使用collection标签封装

实现
1) 修改User类

User类中要有List<Account>,用于保存用户拥有的帐号信息集合

public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    /**因为一个用户有多个帐号,所以这里要有Account的集合*/
    private List<Account> accounts;
 	
    //get/set...
}
2) 在映射器接口UserDao中增加方法
package com.is.fwis.dao;

import com.is.fwis.domain.User;

import java.util.List;

/**
 * @author lcz
 * @date 2021/12/07
 */
public interface UserDao {

    List<User> queryAllUser();
}
3) 在配置文件UserDao.xml中增加statement
  • 封装结果集时,要使用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.is.fwis.dao.UserDao">
    <select id="queryAllUser" resultMap="userMap">
       SELECT u.*, a.id aid, a.uid, a.money FROM USER u LEFT JOIN account a ON u.id = a.uid
    </select>
    <resultMap id="userMap" type="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="birthday" column="birthday"/>
        <result property="sex" column="sex"/>
        <result property="address" column="address"/>

        <collection property="accounts" ofType="Account">
            <id property="id" column="aid"/>
            <result property="uid" column="uid"/>
            <result property="money" column="money"/>
        </collection>
    </resultMap>
</mapper>
4) 在单元测试类中编写测试代码
    /**
     * 查询所有用户,及关联的帐号列表。
     * 要求:使用一条   多表查询语句  实现
     */
    @Test
    public void testQueryAllUser(){
        List<User> userList = userDao.queryAllUser();
        userList.forEach(System.out::println);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如你i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值