Back

MyBatis(二)

MyBatis进阶使用

MyBatis(二)

MyBatis resultMap元素

resultMap是MyBatis中最复杂的元素,主要用于解决实体类属性名与数据库表中字段名不一致的情况,可以将查询结果映射成实体对象。下面我们先从最简单的功能开始介绍

现有的MyBatis版本只支持resultMap查询,不支持更新或者保存,更不必说级联的更新、删除和修改。

  • resultMap元素的构成

    resultMap元素可以包含以下子元素,代码如下

    <resultMap id = "" type = "" >
        <!--类在实例化时用来注入结果的构造方法-->
    	<constructor>
            <!--ID是参数,结果为ID-->
          <idArg></idArg>
            <!--注入到构造方法的一个普通结果-->
            <arg></arg>
        </constructor>
        <!-- 用于表示哪个列是主键 -->
        <id></id>
        <!--注入到字段或JavaBean属性的普通结果-->
        <result></result>
        <!--用于一对一关联-->
        <association property = ""></association>
        <!--用于一对多、多对多关联-->
        <collection property = ""></collection>
    	<!--使用结果值来决定使用哪个结果映射-->
        <discriminator javaType = "">
          <!--基于某些值的结果映射-->
            <case value = ""></case>
        </discriminator>
    </resultMap>
    

    其中:

    • <resultMap> 元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识。
    • 子元素 <constructor> 用于配置构造方法。当一个 POJO 没有无参数构造方法时使用。
    • 子元素 <id> 用于表示哪个列是主键。允许多个主键,多个主键称为联合主键。
    • 子元素<result>用于表示 POJO 和 SQL 列名的映射关系。
    • 子元素 <association><collection> <discriminator> 在级联的情况下使用

    <id><result>元素都有以下属性

    1. property: 映射到列结果的字段或属性。如果 POJO 的属性和 SQL 列名(column元素)是相同的,那么 MyBatis 就会映射到 POJO 上
    2. column:对应SQL列
    3. javaType:配置Java类型。可以是特定的类完全限定名或MyBatis上下文的别名
    4. jdbcType:配置数据库类型。这是JDBC类型,MyBatis已经为我们做了限定,基本支持所有常用的数据库类型
    5. typeHandler:类型处理器。允许你用特定的处理器来覆盖MyBatis默认的处理器。需要指定jdbcType和javaType相互转化的规则

一条 SQL 查询语句执行后会返回结果集,结果集有两种存储方式,即使用 Map 存储和使用 POJO 存储。

  • 使用Map存储结果集

    任何select语句都可以使用Map存储,代码如下

    UserMapper.java

    List<Map<String,Object>> selectUserById(int id);
    

    UserMapper.xml

    <select id="selectUserById" resultType="map">
        select * from user where id = #{id}
    </select>
    

    Map的key是select语句查询的字段名(必须完全一样),而Map的value是查询返回结果中字段对应的值,一条记录映射到一个Map对象中。

    使用Map存储结果集很方便,但可读性稍差,所以一般推荐使用POJO的方式存储

  • 使用POJO存储结果集

    因为MyBatis提供了自动映射,所以使用POJO存储结果集是最常用的方式。但有时候需要更加复杂的映射或级联,这时就需要使用select元素的resultMap属性配置映射集合。

    UserMapper.XML代码如下

    <resultMap id="myResult" type="com.heng.pojo.User">
        <id property="id" column="id"></id>
        <result property="name" column="name"></result>
    </resultMap>
    

    resultMap 元素的属性 id 代表这个 resultMap 的标识,type 标识需要映射的 POJO。我们可以使用 MyBatis 定义好的类的别名或自定义类的全限定名。

    这里使用 property 元素指定 Website 的属性名称 uname,column 表示数据库中 website 表的 SQL 列名 name,将 POJO 和 SQL 的查询结果一 一对应。

    <select id="selectAllByResultMap" resultMap="myResult">
        select id,name from user
    </select>
    

    可以发现 SQL 语句的列名和 myResult 中的 column 一一对应。

  • resultType和resultMap的区别

    MyBatis 的每一个查询映射的返回类型都是 resultMap,只是当我们提供的返回类型是 resultType 时,MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性,而当我们提供的返回类型是 resultMap 时,MyBatis 会将数据库中的列数据复制到对象的相应属性上,可用于复制查询。

    需要注意的是,resultMap 和 resultType 不能同时使用。

日志工厂

思考:当我们在测试SQL的时候,要是能够在控制台输出SQL的话,是不是就能够有更快的排错效率?

如果一个数据库相关的操作出现了问题,我们可以根据输出的SQL语句快速排查问题。

对于以往的开发过程,我们会经常使用debug模式来调节,跟踪我们的代码执行过程。但是现在使用MyBatis开发是基于接口的,配置文件的源代码执行过程。因此,我们必须选择日志工具来作为我们开发,调节程序的工具

MyBatis内置的日志工厂提供日志功能,具体的实现有以下几种工具:

  • STDOUT_LOGGING(标准日志实现)

  • SLF4J

  • Apache Commons Logging

  • Log4j 2

  • Log4j(最常用的日志工具)

  • JDK logging

标准日志实现

指定 MyBatis 应该使用哪个日志记录实现。如果此设置不存在,则会自动发现日志记录实现

<settings>
       <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

测试,可以看到控制台有大量的输出!我们可以通过这些输出来判断程序到底哪里出了Bug

image08

Log4J

简介:

  • Log4j是Apache的一个开源项目
  • 通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件….
  • 我们也可以控制每一条日志的输出格式;
  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

使用步骤:

  1. 导log4j的包

    <!-- https://mvnrepository.com/artifact/log4j/log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
    
  2. 配置文件编写

    #将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
    log4j.rootLogger=DEBUG,console,file
    
    #控制台输出的相关设置
    log4j.appender.console = org.apache.log4j.ConsoleAppender
    log4j.appender.console.Target = System.out
    log4j.appender.console.Threshold=DEBUG
    log4j.appender.console.layout = org.apache.log4j.PatternLayout
    log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
    
    #文件输出的相关设置
    log4j.appender.file = org.apache.log4j.RollingFileAppender
    log4j.appender.file.File=./log/heng.log
    log4j.appender.file.MaxFileSize=10mb
    log4j.appender.file.Threshold=DEBUG
    log4j.appender.file.layout=org.apache.log4j.PatternLayout
    log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
    
    #日志输出级别
    log4j.logger.org.mybatis=DEBUG
    log4j.logger.java.sql=DEBUG
    log4j.logger.java.sql.Statement=DEBUG
    log4j.logger.java.sql.ResultSet=DEBUG
    log4j.logger.java.sql.PreparedStatement=DEBUG
    
  3. setting设置日志实现

    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
    
  4. 在程序中使用Log4j进行输出

    import org.apache.log4j.Logger;
    class Log4JTest{
        static Logger logger = Logger.getLogger(MyTest.class);
        @Test
        public void testLog4j(){
            logger.info("info:进入了testLog4j方法");
            logger.debug("debug:进入了testLog4j方法");
            logger.error("error:进入了testLog4j方法");
            SqlSession session = MybatisUtils.getSession();
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<User> users = mapper.selectUser();
            for (User user : users) {
                System.out.println(user);
            }
            session.close();
        }
    }
    
    
  5. 测试,看控制台输出!

    • 使用Log4j 输出日志
    • 可以看到还生成了一个日志的文件 【需要修改file的日志级别】

    image09

MyBatis实现分页

为什么需要分页?

​ 在学习MyBatis等持久层框架的时候,会经常对数据进行增、删、改、查操作,使用最多的是对数据库进行查询操作,在查询大量数据的时候,我们往往会使用分页进行查询,也就是每次处理小部分数据,这样对数据库压力就在可控范围。

  • 使用Limit实现分页

    #语法
    #startIndex:分页开始的页码;pageSize:分页的大小(分多少页)
    select * from user limit startIndex,pageSize
    #查询第6到10行记录(检索行6-10)
    select * from user limit 5,5
    #为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1
    #检索第91行到最后一行的数据(91-last)
    select * from user limit 90,-1
    #如果给定一个参数,它表示返回最大的记录行数目
    #检索前5行记录(0-5)
    select * from user limit 5
    #换句话说,LIMIT n 等价于 LIMIT 0,n。
    

    步骤:

    1. 修改UserMapper接口,或使用注解

      List<User> selectUserByLimit(Map<String, Integer> map);
      

      使用注解

      @Select("select * from user limit #{startIndex},#{pageSize}")
      @ResultMap(value = "userMapper")
      public List<User> seletUserByLimit(Map<String, Integer> map);
      
    2. 修改Mapper文件(使用注解不需要)

      <select id="selectUserByLimit" resultMap="myResult" parameterType="map">
          select * from user limit #{startIndex},#{pageSize}
      </select>
      
    3. 在测试类进行传参测试

      @Test
      public void selectUserByLimit(){
          SqlSession session = MybatisUtils.getSession();
          UserMapper mapper = session.getMapper(UserMapper.class);
          HashMap<String, Integer> map = new HashMap<>();
          map.put("startIndex",2);
          map.put("pageSize",2);
          List<User> users = mapper.selectUserByLimit(map);
          for (User user : users) {
              System.out.println(user);
          }
          session.close();
      }
      
    4. 测试程序,输出结果

      image10

  • RowBounds分页

    我们除了使用Limit在SQL层面实现分页,也可以使用RowBounds在Java代码层面实现分页,当然此种方式作为了解即可。我们来看下如何实现的!

    步骤:

    1. Mapper接口

      //选择全部用户RowBounds实现分页
      List<User> getUserByRowBounds();
      
    2. mapper文件

      <select id="getUserByRowBounds" resultType="user">
      select * from user
      </select>
      
    3. 测试类

      在这里,我们需要使用RowBounds类

      @Test
      public void testUserByRowBounds() {
         SqlSession session = MybatisUtils.getSession();
      
         int currentPage = 2;  //第几页
         int pageSize = 2;  //每页显示几个
         RowBounds rowBounds = new RowBounds((currentPage-1)*pageSize,pageSize);
      
         //通过session.**方法进行传递rowBounds,[此种方式现在已经不推荐使用了]
         List<User> users = session.selectList("com.heng.mapper.UserMapper.getUserByRowBounds", null, rowBounds);
      
         for (User user: users){
             System.out.println(user);
        }
         session.close();
      }
      
  • PageHelper实现分页

    官方文档:https://pagehelper.github.io/

MyBatis注解

MyBatis注解本质是反射实现!底层为动态代理模式

为了简化XML的配置,MyBatis提供了注解。我们可以通过MyBatis的jar包查看注解,如下图所示

image07

以上注解主要分为三大类,即SQL语句映射、结果集映射和关系映射。下面将分别进行讲解

  1. SQL语句映射

    • @Insert:实现新增功能

      @Insert("insert into user(id,name) values(#{id},#{name})")
      public int insert(User user);
      
    • @Select:实现查询功能

      @Select("Select * from user")
      @Results({
          @Result(id = true,column = "id" , property = "id"),
          @Result(id = true,column = "name" , property = "name"),
          @Result(id = true,column = "pwd" , property = "pwd")
      })
      List<User> queryAllUser();
      
    • @SelectKey:插入后,获取id的值

      以MySQL为例,MySQL在插入一条数据后,使用select last_insert_id()可以获取到自增id的值

      @Insert("insert into user(id,name) values(#{id},#{name})")
      @SelectKey(statement = "select last_insert_id",keyProperty = "id",KeyColumn = "id",resultType = int,before = false)
      public int insert(User user);
      

      @SelectKey各个属性含义如下

      • statement:表示要运行的SQL语句
      • keyProperty:可选项,表示将查询结果赋值给数据表中的哪一列;
      • keyColumn:可选项,表示将查询结果赋值给数据表中的哪一列;
      • resultType:指定SQL语句的返回值;
      • before:默认值为true,在执行插入语句之前,执行select last_insert_id()。值为false,则在执行插入语句之后,执行select last_insert_id()。
    • @Update:实现更新功能

      @Update("Update user set name = #{name},pwd = #{pwd} where id = #{id}")
      public void updateUserById(User user);
      
    • @delete:实现删除功能

      @ddelete("delete from user where id = #{id}")
      public void deleteUserById(Integer id);
      
    • @Param:映射多个参数

      @Param用于在Mapper接口中映射多个参数

      int saveUser(@Param(value="user") User user,@Param("name") String name,@Param("pwd") String pwd);
      

      @Param 中的 value 属性可省略,用于指定参数的别名 。

      关于@Param注解:

      • 基本类型的参数或者String类型的参数都需要加上这个注解
      • 引用类型不需要加
  2. 结果集映射

    @Result、@Results、@ResultMap 是结果集映射的三大注解。

    声明结果集映射关系代码:

    @Select({"select id, name, class_id from student"})
    @Results(id="studentMap", value={
        @Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
        @Result(column="name", property="name", jdbcType=JdbcType.VARCHAR),
        @Result(column="class_id ", property="classId", jdbcType=JdbcType.INTEGER)
    })
    List<Student> selectAll();
    

    下面为 @Results 各个属性的含义。

    • id:表示当前结果集声明的唯一标识;
    • value:表示结果集映射关系;
    • @Result:代表一个字段的映射关系。其中,column 指定数据库字段的名称,property 指定实体类属性的名称,jdbcType 数据库字段类型,id 为 true 表示主键,默认 false。

    可使用 @ResultMap 来引用映射结果集,其中 value 可省略。

    @Select({"select id, name, class_id from student where id = #{id}"})
    @ResultMap(value="studentMap")
    Student selectById(Integer id);
    

    这样不需要每次声明结果集映射时都复制冗余代码,简化开发,提高了代码的复用性。

  3. 关系映射

    • @one:用于一对一关系映射

      @Select("select * from student") 
      @Results({ 
          @Result(id=true,property="id",column="id"), 
          @Result(property="name",column="name"), 
          @Result(property="age",column="age"), 
          @Result(property="address",column="address_id",one=@One(select="net.biancheng.mapper.AddressMapper.getAddress")) 
      }) 
      public List<Student> getAllStudents();  
      
    • @many:用于一对多关系映射

      @Select("select * from t_class where id=#{id}") 
      @Results({ 
          @Result(id=true,column="id",property="id"), 
          @Result(column="class_name",property="className"), 
          @Result(property="students", column="id", many=@Many(select="net.biancheng.mapper.StudentMapper.getStudentsByClassId")) 
          }) 
      public Class getClass(int id); 
      

MyBatis关联(级联)查询

级联关系是一个数据库实体的概念,有3种级联关系,分别是一对一级联、一对多级联以及多对多级联。例如,一个角色可以分配给多个用户,也可以只分配给一个用户。大部分场景下,我们都需要获取角色信息和用户信息,所以会经常遇见一下SQL:

SELECT r.*,u.* FROM t_role r
INNER JOIN t_user_role ur ON r.id = ur.id
INNER JOIN t_user u ON ur.user_id = u.id
WHERE r.id = #{id}

在级联中存在三种对应的关系。

  • 一对多的关系,如角色和用户的关系。通俗的理解就是,一家软件公司会存在许多软件工程师,公司和软件工程师就是一对多的关系。
  • 一对一的关系。每个软件工程师都有一个编号(ID),这是他在公司的标识,它与工程师是一对一的关系。
  • 多对多的关系,有一些公司一个角色可以对应多个用户,但是一个用户可以兼任多个角色。通俗的说,一个人既可以是总经理,同时也是技术总监,而技术总监这个职位可以对应多个人,这就是多对多的关系。

实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。推荐的方法是,用一对多的关系把它分解为双向关系,以降低关系的复杂度,简化程序。

级联的优点是获取关联数据十分便捷。但是级联过多会增加系统的复杂度,同时降低系统的性能,此增彼减。所以记录超过 3 层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、负载和难以维护。

MyBatis一对一关联查询

一对一级联关系在现实生活中是非常常见的,例如一个大学生只有一个学号,一个学号只属 于一个学生。同样,人与身份证也是一对一的级联关系。

在MyBatis中,通过<resultMap>元素的子元素<association>处理一对一级联关系。实例代码如下:

<association property="studentCard" column="cardId" 
             javaType="net.biancheng.po.StudentCard"
         select="net.biancheng.mapper.StudentCardMapper.selectStuCardById"></association>

<association>元素中通常使用以下属性:

  • property:指定映射到实体类的对象属性
  • column:指定表中对应的字段(即查询返回的列名)。
  • javaType:指定映射到实体对象属性的类型。
  • select:指定引入嵌套查询的子SQL语句,该属性用于关联映射中的嵌套查询

一对一关联查询可采用以下两种方式:

  • 单步查询,通过关联查询实现
  • 分步查询,通过两次或多次查询,为一对一关系的实体Bean赋值

MyBatis多对一关联查询

多对一关联查询其实就是一对一关联查询的衍生;

例如:

  • 多个学生对应一个老师

  • 如果对于学生这边,就是一个多对一的现象,即从学生这边关联一个老师!

  • 在数据库中关系图如下

    image12

示例

查询学生的所有信息,包括对应老师的姓名

基于查询嵌套处理关联

  1. 设计MySQL

    CREATE TABLE `teacher` (
    `id` INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8
    
    INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师');
    
    CREATE TABLE `student` (
    `id` INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    `tid` INT(10) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `fktid` (`tid`),
    CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
    ) ENGINE=INNODB DEFAULT CHARSET=utf8
    
    
    INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1');
    INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1');
    INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1');
    INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1');
    INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');
    
  2. 创建实体类Student以及Teacher

    Srudent类

    package com.heng.pojo;
    
    import lombok.Data;
    
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:27
     */
    //使用lombok的Data注解可以帮我们自动配置构造器、getter和setter方法
    @Data
    public class Student {
        private int id;
        private String name;
        private Teacher teacher;
    }
    

    Teacher类

    package com.heng.pojo;
    
    import lombok.Data;
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:26
     */
    @Data
    public class Teacher {
        private int id;
        private String name;
    
    }
    
  3. 创建StudentMapper接口,在此接口增加方法

    package com.heng.dao;
    
    import com.heng.pojo.Student;
    import org.apache.ibatis.annotations.Param;
    
    import java.util.List;
    
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:28
     */
    @SuppressWarnings({"all"})
    public interface StudentMapper {
        //查询所有的学生信息,以及对应的老师信息
        List<Student> selectStudentAll();
    }
    
  4. 编写对应的Mapper.xml配置文件

    <?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.heng.dao.StudentMapper">
        <!--
    	需求:获取所有学生及对应老师的信息
    	思路:
    	1.获取所有学生的信息
    	2.根据获取到的学生信息的tid->获取该老师的信息
         3.查询出来的学生结果集中包含了老师,我们需要使用关联查询来处理结果集
         3.1 做一个结果集映射TeacherResult
         3.2 TeacherResult结果集的类型为Student
         3.3 在学生表中,有一个Teacher类的属性teacher,让其对应数据库中的tid
         3.4 利用association标签来处理一个复杂类型的关联;使用它来处理关联查询
    	-->
        <select id="selectStudentAll" resultMap="TeacherResult" >
            select * from student
        </select>
        <resultMap id="TeacherResult" type="com.heng.pojo.Student">
            <result column="id" property="id"></result>
            <result column="name" property="name"></result>
            <association property="teacher" column="tid" javaType="com.heng.pojo.Teacher" select="selectTeacher">
            </association>
        </resultMap>
        <select id="selectTeacher" resultType="com.heng.pojo.Teacher">
            select * from teacher where id = #{tid}
        </select>
    </mapper>
    

    association关联的属性:、

    • property属性名

    • javaType属性类型

    • column在多的一方的表中的列名

      column多参数配置:

      column="{key=value,key=value}" 其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。

  5. 测试

    @Test
    public void testSelectAllStudent(){
        SqlSession session = MybatisUtils.getSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);
        List<Student> studentList = mapper.selectStudentAll();
        for (Student student : studentList) {
            System.out.println(student);
        }
        session.close();
    }
    
  6. 运行结果

    image13

按结果嵌套处理

思路:直接查询出结果,进行结果集映射;查出来的数据需要起别名,不然两个实体类都会被同一个表数据映射

  1. 修改StudentMapper接口

    package com.heng.dao;
    
    import com.heng.pojo.Student;
    import org.apache.ibatis.annotations.Param;
    
    import java.util.List;
    
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:28
     */
    @SuppressWarnings({"all"})
    public interface StudentMapper {
        //查询所有的学生信息,以及对应的老师信息
        List<Student> selectStudentAll2();
    }
    
  2. 修改Mapper配置文件

    <?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.heng.dao.StudentMapper">
        <!--基于查询结果嵌套处理-->
        <select id="selectStudentAll2" resultMap="teacherResult">
            select s.id sid,s.name sname,t.name tname
            from student s,teacher t
            where s.tid = t.id;
        </select>
        <resultMap id="teacherResult" type="com.heng.pojo.Student">
            <result property="id" column="sid"></result>
            <result property="name" column="sname"></result>
            <association property="teacher" javaType="com.heng.pojo.Teacher">
                <result property="name" column="tname"></result>
            </association>
        </resultMap>
    </mapper>
    
  3. 编写测试类

    @Test
    public void testSelectAllStudent2(){
        SqlSession session = MybatisUtils.getSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);
        List<Student> studentList = mapper.selectStudentAll2();
        for (Student student : studentList) {
            System.out.println("Student = "+student.getName()+" Teacher = "+student.getTeacher().getName());
        }
        session.close();
    }
    
  4. 运行结果:

    image14

MyBatis一对多关联查询(多对多关系的拆分理解)

示例:查询一个老师的多个学生信息

按查询结果嵌套处理

  • 思路:
  1. 从学生表和老师表中查出学生id,学生姓名,老师姓名

  2. 对查询出来的操作做结果集映射 1. 集合的话,使用collection! 2. JavaType和ofType都是用来指定对象类型的 3. JavaType是用来指定pojo中属性的类型 4. ofType指定的是映射到list集合属性中pojo的类型

  3. 修改TeacherMapper接口

    package com.heng.dao;
    
    
    import com.heng.pojo.Teacher;
    import org.apache.ibatis.annotations.Param;
    
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:28
     */
    @SuppressWarnings({"all"})
    public interface TeacherMapper {
        public Teacher getTeacher(@Param("tid") int id);
    }
    
  4. 修改TeacherMapper配置文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.heng.dao.TeacherMapper">
        <!--先把查询结果查询出来,再根据结果映射-->
        <select id="getTeacher" resultMap="TeacherStudent">
            select s.id sid,s.name sname,t.id tid,t.name tname
            from student s,teacher t
            where s.tid=t.id and tid = #{tid}
        </select>
        <resultMap id="TeacherStudent" type="com.heng.pojo.Teacher">
            <result property="id" column="tid"></result>
            <result property="name" column="tname"></result>
            <collection property="students" ofType="com.heng.pojo.Student">
                <result property="id" column="sid"></result>
                <result property="name" column="sname"></result>
                <result property="tid" column="tid"></result>
            </collection>
        </resultMap>
    </mapper>
    
  5. 测试

    @Test
    public void getCourse(){
        SqlSession session = MybatisUtils.getSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);
        List<Student> studentList = mapper.getStudentCourse(2);
        for (Student student : studentList) {
            System.out.println(student);
            System.out.println(student.getCourse().getName());
        }
        session.close();
    }
    
  6. 输出结果

    image15

按查询嵌套处理

  1. 编写接口方法

    package com.heng.dao;
    
    import com.heng.pojo.Teacher;
    import org.apache.ibatis.annotations.Param;
    
    /**
     * @Author: minster
     * @Date: 2021/10/29 15:28
     */
    @SuppressWarnings({"all"})
    public interface TeacherMapper {
        public Teacher getTeacher2(@Param("tid") int id);
    }
    
  2. 修改配置文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.heng.dao.TeacherMapper">
        <select id="getTeacher2" resultMap="TeacherStudent2">
            select * from teacher where id = #{tid}
        </select>
        <!--column是一对多的外键 , 写的是一的主键的列名-->
        <resultMap id="TeacherStudent2" type="com.heng.pojo.Teacher">
            <result property="id" column="id"></result>
            <result property="name" column="name"></result>
            <collection property="students" javaType="ArrayList" ofType="com.heng.pojo.Student" select="getStudent" column="id">
            </collection>
        </resultMap>
        <select id="getStudent" resultType="com.heng.pojo.Student">
            select * from student where tid = #{tid}
        </select>
    </mapper>
    
  3. 测试

    @Test
    public void getTeacher2(){
        SqlSession session = MybatisUtils.getSession();
        TeacherMapper mapper = session.getMapper(TeacherMapper.class);
        Teacher teacher = mapper.getTeacher2(1);
        System.out.println(teacher.getName());
        System.out.println(teacher.getStudents());
        session.close();
    }
    

小结

  1. 关联-association

  2. 集合-collection

  3. 所以association是用于一对一和多对一,而collection是用于一对多的关系

    • JavaType和ofType都是用来指定对象类型的
    • JavaType是用来指定pojo中属性的类型
    • ofType指定的是映射到list集合属性中pojo的类型。

注意说明:

  1. 保证SQL的可读性,尽量通俗易懂

  2. 根据实际要求,尽量编写性能更高的SQL语句

  3. 注意属性名和字段不一致的问题

  4. 注意一对多和多对一 中:字段和属性对应的问题

  5. 尽量使用Log4j,通过日志来查看自己的错误

动态SQL

什么是动态SQL:动态SQL指的是根据不同的查询条件,生成不同的Sql语句。

​ MyBatis的强大特性之一便是它的动态SQL。如果你有使用JDBC或其它类似框架的经验,你就能体会到根据不同条件拼接SQL语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。

​ 利用SQL并非一件易事,但正是MyBatis提供了可以被用在任意SQL映射语句中的强大的动态SQL语言得以改进这种情形。

​ 动态SQL元素和JSTL或基于类似XML的文本处理器相似。在MyBatis之前的版本中,有很多元素需要花时间了解。MyBatis3替换了之前的大部分元素,大大精简了元素种类,现在学习的元素种类比原来的一半还要少。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

——《MyBatis官方文档》

我们之前写的 SQL 语句都比较简单,如果有比较复杂的业务,我们需要写复杂的 SQL 语句,往往需要拼接,而拼接 SQL ,稍微不注意,由于引号,空格等缺失可能都会导致错误。

那么怎么去解决这个问题呢?这就要使用 mybatis 动态SQL,通过 if, choose, when, otherwise, trim, where, set, foreach等标签,可组合成非常灵活的SQL语句,从而在提高 SQL 语句的准确性的同时,也大大提高了开发人员的效率。

  1. 搭建环境:新建表Blog,并插入数据

    CREATE TABLE `blog` (
    `id` varchar(50) NOT NULL COMMENT '博客id',
    `title` varchar(100) NOT NULL COMMENT '博客标题',
    `author` varchar(30) NOT NULL COMMENT '博客作者',
    `create_time` datetime NOT NULL COMMENT '创建时间',
    `views` int(30) NOT NULL COMMENT '浏览量'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    
    @Test
    public void addInitBlog(){
       SqlSession session = MybatisUtils.getSession();
       BlogMapper mapper = session.getMapper(BlogMapper.class);
    
       Blog blog = new Blog();
       blog.setId(IDUtil.genId());
       blog.setTitle("Mybatis如此简单");
       blog.setAuthor("狂神说");
       blog.setCreateTime(new Date());
       blog.setViews(9999);
    
       mapper.addBlog(blog);
    
       blog.setId(IDUtil.genId());
       blog.setTitle("Java如此简单");
       mapper.addBlog(blog);
    
       blog.setId(IDUtil.genId());
       blog.setTitle("Spring如此简单");
       mapper.addBlog(blog);
    
       blog.setId(IDUtil.genId());
       blog.setTitle("微服务如此简单");
       mapper.addBlog(blog);
    
       session.close();
    }
    
  • IF语句

    需求:根据作者名字和博客名字来查询博客!如果作者名字为空,name只根据博客名字查询,反之,则根据作者名来查询

    <!--需求1:
    根据作者名字和博客名字来查询博客!
    如果作者名字为空,那么只根据博客名字查询,反之,则根据作者名来查询
    select * from blog where title = #{title} and author = #{author}
    -->
    <select id="queryBlogIf" parameterType="map" resultType="blog">
      select * from blog where
       <if test="title != null">
          title = #{title}
       </if>
       <if test="author != null">
          and author = #{author}
       </if>
    </select>
    

    这样写我们可以看到,如果 author 等于 null,那么查询语句为 select * from user where title=#{title},但是如果title为空呢?那么查询语句为 select * from user where and author=#{author},这是错误的 SQL 语句,如何解决呢?请看下面的 where 语句!

  • Where

    <select id="queryBlogIf" parameterType="map" resultType="blog">
      select * from blog
       <where>
           <if test="title != null">
              title = #{title}
           </if>
           <if test="author != null">
              and author = #{author}
           </if>
       </where>
    </select>
    

    这个“where”标签会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。此外,如果标签返回的内容是以AND 或OR 开头的,则它会剔除掉。

  • Set

    同理,上面的对于查询 SQL 语句包含 where 关键字,如果在进行更新操作的时候,含有 set 关键词,我们怎么处理呢?

    <!--注意set是用的逗号隔开-->
    <update id="updateBlog" parameterType="map">
      update blog
         <set>
             <if test="title != null">
                title = #{title},
             </if>
             <if test="author != null">
                author = #{author}
             </if>
         </set>
      where id = #{id};
    </update>
    
  • choose语句

    有时候,我们不想用到所有的查询条件,只想选择其中的一个,查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句

    <select id="queryBlogChoose" parameterType="map" resultType="blog">
      select * from blog
       <where>
           <choose>
               <when test="title != null">
                    title = #{title}
               </when>
               <when test="author != null">
                  and author = #{author}
               </when>
               <otherwise>
                  and views = #{views}
               </otherwise>
           </choose>
       </where>
    </select>
    

    注意:这里的When会由上到下按顺序执行,只要有一个成功执行,后面的都不会执行了!

    @Test
    public void selectByChoose(){
        SqlSession session = MybatisUtils.getSession();
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        Map map = new HashMap();
        map.put("author","jack");
        map.put("title","如何让富婆爱上我");
        map.put("views","1000");
        List<Blog> blogs = mapper.selectByChoose(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        session.close();
    }
    

    image16

所谓的动态SQL,本质上还是SQL语句,只是我们可以在SQL层面,去执行逻辑代码

  • Foreach

    foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

    • collection:指定输入对象中的集合属性
    • item:每次遍历生成的对象
    • open:开始遍历时的拼接字符串
    • close:结束时拼接的字符串
    • separator:遍历对象之间需要拼接的字符串

    示例代码

    <select id="queryBlogByForEach" parameterType="map" resultType="com.heng.pojo.Blog">
        select * from blog
        <where>
            <foreach collection="ids" item="id" open="and (" close=")" separator="or">
                id = #{id}
            </foreach>
        </where>
    </select>
    

    测试

    @Test
    public void queryBlogByForEach(){
        SqlSession session = MybatisUtils.getSession();
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        Map map = new HashMap();
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        map.put("ids",list);
        List<Blog> blogs = mapper.queryBlogByForEach(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        session.close();
    }
    

    image17

SQL片段

有时候可能某个SQL语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。

提取SQL片段

<sql id="if-title-author">
   <if test="title != null">
      title = #{title}
   </if>
   <if test="author != null">
      and author = #{author}
   </if>
</sql>

引用SQL片段

<select id="queryBlogIf" parameterType="map" resultType="blog">
  select * from blog
   <where>
       <!-- 引用 sql 片段,如果refid 指定的不在本文件中,那么需要在前面加上 namespace -->
       <include refid="if-title-author"></include>
       <!-- 在这里还可以引用其他的 sql 片段 -->
   </where>
</select>

注意:

  • 最好基于单表来定义SQL片段,提高片段的可重用性
  • 在SQL片段中不要包括where

小结:

其实动态 sql 语句的编写往往就是一个拼接的问题,为了保证拼接准确,我们最好首先要写原生的 sql 语句出来,然后在通过 mybatis 动态sql 对照着改,防止出错。多在实践中使用才是熟练掌握它的技巧。

缓存

  1. 概括

    什么是缓存[Cache]

    • 存在内存中的临时数据。
    • 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从硬盘上(关系型数据库/数据文件)查询,从缓存中查询,从而提高了查询效率,解决了高并发系统的性能问题。

    为什么使用缓存

    • 减少和数据库交互次数,减少系统开销

    什么样的数据能使用缓存

    • 经常查询并且不经常改变的数据

MyBatis缓存

  • MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
  • MyBatis系统中默认定义了两级缓存:一级缓存二级缓存
    • 默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存)
    • 二级缓存需要手动开启和配置,它是基于namespace级别的缓存。
    • 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
  1. 一级缓存

    一级缓存也叫本地缓存

    • 与数据库同一次会话期间查询到的数据会妨碍本地缓存中
    • 以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库

    代码示例

    1. 在MyBatis中开启日志,方便测试结果

    2. 编写接口方法

      //根据id查询用户
      User queryUserById(@Param("id") int id);
      
    3. 接口对应的Mapper文件

      <select id="queryUserById" resultType="user">
        select * from user where id = #{id}
      </select>
      
    4. 测试

      @Test
      public void testQueryUserById(){
         SqlSession session = MybatisUtils.getSession();
         UserMapper mapper = session.getMapper(UserMapper.class);
      
         User user = mapper.queryUserById(1);
         System.out.println(user);
         User user2 = mapper.queryUserById(1);
         System.out.println(user2);
         System.out.println(user==user2);
      
         session.close();
      }
      
    5. 结果分析

      image18

    一级缓存失效的四种情况

    一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它;

    一级缓存失效情况:程序没有使用到当前的一级缓存,还需要再向数据库发起一次查询请求!

    1. SqlSession不同

      @Test
      public void testQueryUserById(){
         SqlSession session = MybatisUtils.getSession();
         SqlSession session2 = MybatisUtils.getSession();
         UserMapper mapper = session.getMapper(UserMapper.class);
         UserMapper mapper2 = session2.getMapper(UserMapper.class);
      
         User user = mapper.queryUserById(1);
         System.out.println(user);
         User user2 = mapper2.queryUserById(1);
         System.out.println(user2);
         System.out.println(user==user2);
      
         session.close();
         session2.close();
      }
      

      观察结果:发现请求了两条SQL语句!

      结论:每个SqlSession中的缓存相互独立

    2. SqlSession相同,查询条件不同

      @Test
      public void queryUserById(){
          SqlSession session = MybatisUtils.getSession();
          UserMapper mapper = session.getMapper(UserMapper.class);
      
          User user1 = mapper.queryUserById(1);
          System.out.println("user1:"+user1);
          User user2 = mapper.queryUserById(2);
          System.out.println("user2:"+user2);
          System.out.println(user1==user2);
          session.close();
      }
      

      观察结果:发现发送了两条SQL语句!很正常的理解

      结论:当前缓存中,不存在这个数据

    3. SqlSession相同,两次查询之间执行了增、删、改操作

      @Test
      public void testQueryUserById(){
         SqlSession session = MybatisUtils.getSession();
         UserMapper mapper = session.getMapper(UserMapper.class);
      
         User user = mapper.queryUserById(1);
         System.out.println(user);
      
         HashMap map = new HashMap();
         map.put("name","kuangshen");
         map.put("id",4);
         mapper.updateUser(map);
      
         User user2 = mapper.queryUserById(1);
         System.out.println(user2);
      
         System.out.println(user==user2);
      
         session.close();
      }
      

      观察结果:查询在中间执行了增删改操作后,从新执行了(增删改回刷新缓存!)

      结论:因为增删改操作可能会对当前数据产生影响

    4. SqlSession相同,手动清除了一级缓存

      @Test
      public void queryUserById(){
          SqlSession session = MybatisUtils.getSession();
          UserMapper mapper = session.getMapper(UserMapper.class);
      
          User user1 = mapper.queryUserById(1);
          System.out.println("user1:"+user1);
          session.clearCache();
          User user2 = mapper.queryUserById(1);
          System.out.println("user2:"+user2);
          System.out.println(user1==user2);
          session.close();
      }
      

      image19

      一级缓存就是一个map

  2. 二级缓存

    • 二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存

    • 基于namespace级别的缓存;一个名称空间,对应一个二级缓存

    • 工作机制

      • 一个会话查询了一条数据,这个会话就会被放在当前会话的一级缓存中;
      • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据就会被保存到二级缓存中;
      • 新的会话查询信息,就可以从二级缓存中获取内容
      • 不同的mapper查出的数据会仿真自己对应的缓存(map)中

      image20

    使用步骤

    1. 开启全局缓存

      <setting name="cacheEnabled" value="true"/>
      
    2. 去每个mapper.xml中配置使用二级缓存,这个配置非常简单;

      <cache></cache>
      <!--等价于-->
          <cache
              eviction="FIFO"
              flushInterval="60000"
              size="512"
              readOnly="true"/>
      <!-- FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。-->
      
    3. 测试

      • 所有的实体类需要先实现序列化接口!
       @Test
          public void queryUserById(){
              SqlSession session1 = MybatisUtils.getSession();
              UserMapper mapper1 = session1.getMapper(UserMapper.class);
      
              SqlSession session2 = MybatisUtils.getSession();
              UserMapper mapper2 = session2.getMapper(UserMapper.class);
      
              User user1 = mapper1.queryUserById(1);
              System.out.println("user1:"+user1);
      
              System.out.println("从一级缓存读取数据");
      
              User user2 = mapper1.queryUserById(1);
              System.out.println("user2:"+user2);
      
              session1.close();
      
              System.out.println("====Session1关闭!====");
              System.out.println("从二级缓存读取数据");
      
              User user3= mapper2.queryUserById(1);
              System.out.println("user3:"+user3);
              System.out.println(user1==user2&&user2==user3);
              session2.close();
          }
      

      输出结果

      imaghe21

    结论:

    • 只要开启了二级缓存,我们在同一个Mapper中的查询,可以在二级缓存中拿到数据
    • 查出的数据都会被默认先放在一级缓存中
    • 只有会话提交或者关闭以后,一级缓存中的数据才会转到二级缓存中
  3. 缓存原理图

    image22

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy