MyBatis详细介绍

1 简介

mybatis官网

1.1 什么是Mybatis

MyBatis详细介绍

  • MyBatis 是一款优秀的持久层框架
  • 它支持定制化 SQL、存储过程以及高级映射。
  • MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
  • MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。
  • MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了[google code](https://baike.baidu.com/item/google code/2346604),并且改名为MyBatis 。
  • 2013年11月迁移到Github

获取Mybatis

  • maven仓库

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>
    
  • github : https://github.com/mybatis/mybatis-3/releases

  • 中文文档:https://mybatis.org/mybatis-3/zh/index.html

1.2 特点

  • 简单易学
    • 本身就很小且简单。
    • 没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件易于学习
    • 易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。
  • 灵活:
    • mybatis不会对应用程序或者数据库的现有设计强加任何影响。
    • sql写在xml里,便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求。
  • 松耦合
    • 解除sql与程序代码的耦合
    • 通过提供DAO层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。
    • sql和代码的分离,提高了可维护性。
  • 提供标签
    • 提供映射标签,支持对象与数据库的orm字段关系映射
    • 提供对象关系映射标签,支持对象关系组建维护
    • 提供xml标签,支持编写动态sql。

1.3 持久层

持久化:将数据从瞬时状态转化为持久状态。

之所以有持久层,就是将数据能断电后存储。一些其他原因,比如内存太贵

Dao层就是一个持久层,层的概念是为了解耦合。

MyBatis 是一款优秀的持久层框架

1.4 为什么使用Mybatis

  • 非常方便将数据库存入到数据库中
  • 框架,简化JDBC,自动化
  • 不使用Mybatis也能持久化,但是它更容易、更方便上手
  • 优点:简单易学、灵活、sql与代码分离、可维护性、标签
  • 最重要的一点:使用的人多

2 第一个Mybatis程序

pojos

package com.zl.pojos;

//对应数据库的一张表结构
public class Students {
    private int id;
    private int age;
    private String name;

    public int getId() {
        return id;
    }

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

DAO接口

public interface StudentsMapper {
    public List<Students> getStudents();
}

映射文件

<?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">

<!--namespace对应一个mapper接口-->
<mapper namespace="com.zl.dao.StudentsMapper">
    <select id="getStudents" resultType="com.zl.pojos.Students">
        select * from students
    </select>
</mapper>

核心配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--环境,可以配置多个-->
<environments default="development">
    <environment id="development">
        <!--事务类型-->
        <transactionManager type="JDBC"/>
        <!--type:线程池-->
        <dataSource type="POOLED"> 
            <!--四个配置项-->
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/jdbc_test?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
<mappers>
    <!--绑定映射文件-->
    <mapper resource="com/zl/dao/StudentsMapper.xml"/>
</mappers>
</configuration>

工具类

public class Mybatis_utils {
    //SqlSessionFactory建立单例,可以重复使用,创建SqlSession
    private static SqlSessionFactory sqlSessionFactory;

    static{
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //SqlSessionFactoryBuilder在创建sqlSessionFactory即可删除
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();
    }
}

测试

public class Test {

    @org.junit.Test
    public void test1(){
        //SqlSession不是线程安全的,每次使用完之后应该关闭
        try(SqlSession session = Mybatis_utils.getSqlSession()){
            StudentsMapper students = session.getMapper(StudentsMapper.class);
            List<Students> list = students.getStudents();

            for(Students s: list){
                System.out.println(s);
                System.out.println("");
            }
        }
    }

}

3 CRUD

更新

<insert id="addStudents" parameterType="com.zl.pojos.Students" >
        insert into students (`name`,`age`)  values (#{name}, #{age})
</insert>

<update id="updateStudents" parameterType="com.zl.pojos.Students">
    update students set `name` = #{name},`age` = #{age} where `id` = #{id}
</update>

<delete id="deleteStudents" parameterType="com.zl.pojos.Students">
    delete from `students` where `id` = #{id}
</delete>

测试

public void updateTest(){
    try(SqlSession session = Mybatis_utils.getSqlSession()){
        StudentsMapper mapper = session.getMapper(StudentsMapper.class);
        Students students = new Students(0, 12, "Jane");

        int line = mapper.addStudents(students);

        System.out.println("line:" + line);

        students.setId(1);
        line = mapper.updateStudents(students);
        System.out.println("line:" + line);

        students.setId(2);
        line = mapper.deleteStudents(students);
        System.out.println("line:" + line);

        session.commit();

        List<Students> list = mapper.getStudents();

        for(Students s: list){
            System.out.println(s);
            System.out.println("");
        }

    }
}

parameterType扩展

  • map 使用键值对传参
  • 单个传参查询,无需指定parameterType
  • 模糊查询
<select id="getStudentsById"  resultType="com.zl.pojos.Students">
        select * from `students` where `id` = #{id}
</select>

<select id="getStudentsByName" parameterType="map" resultType="com.zl.pojos.Students">
    select * from `students` where `name` = #{sname}
</select>

<select id="getStudentsLikeName" resultType="com.zl.pojos.Students">
    select * from `students` where `name` like #{name}
</select>

测试

try(SqlSession session = Mybatis_utils.getSqlSession()){
    StudentsMapper students = session.getMapper(StudentsMapper.class);
    List<Students> list = students.getStudentsById(3);

    showList(list);

    Map<String, Object> map = new HashMap<>();
    map.put("sname", "Tom"); //通过键值对,可以多个
    list = students.getStudentsByName(map);

    showList(list);

    list = students.getStudentsLikeName("_o%");

    showList(list);
}

4 配置解析

研究核心配置文件中的属性配置

4.1 环境配置(environments)

MyBatis 可以配置成适应多种环境,比如开发环境、上线环境、测试环境等。

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

<!--default选择默认环境-->
<environments default="development">

事务管理器(transactionManager)

<transactionManager type=""[JDBC|MANAGED]"">
  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域,常用

  • MANAGED - 让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)

    如果使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

数据源(dataSource)

<dataSource type="[UNPOOLED|POOLED|JNDI]">
  • UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。
  • POOLED常用类型选择,这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
  • JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用。在EJB用,目前很少用

4.2 属性(properties)

这些属性可以在外部进行配置,并可以进行动态替换。

<properties resource="org/mybatis/example/config.properties">
  <!--以下的property也可以不写,会自动加载配置文件中的内容-->
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
</properties>

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

配置文件

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useUnicode=true&characterEncoding=utf8&useSSL=true
username=root
password=123456

注意点:核心配置中,各个配置有严格的顺序

The content of element type "configuration" must match "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)".

4.3 设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。

常用的setting配置

<settings>
    <!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存-->
    <setting name="cacheEnabled" value="true"/>
    <!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载-->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!--允许 JDBC 支持自动生成主键,需要数据库驱动支持-->
    <setting name="useGeneratedKeys" value="false"/>
    <!--是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn-->
    <setting name="mapUnderscoreToCamelCase" value="false"/>
    <!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找-->
    <setting name="logImpl" value="JDK_LOGGING "/>
</settings>

4.4 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。

<typeAliases>
    <package name="com.zl.pojos"/>
</typeAliases>

<typeAliases>
    <typeAlias type="com.zl.pojos.Students" alias="stu"/>
</typeAliases>

有两种方式:

  • 指定包名,使用时直接使用类名,Mybatis会在包下搜索需要的Jave Bean
  • 直接为单一类型增加别名,使用时使用别名即可
<!--已指定包名,直接使用students,首字母也可以不用大写-->
<select id="getStudents" resultType="students">
    select * from students
</select>

<!--使用stu-->
<select id="getStudents" resultType="stu">
    select * from students
</select>

4.5 mappers(映射器)

作用:告诉Mybatis到哪里去找寻SQL语句。

方式一:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

方式二:因为是指定类名,所以资源名要与类名同名、且在同一目录下

<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

方式三:同方式二,因为是指定类名,所以资源名要与类名同名、且在同一目录下

<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

4.6 其他配置

4.7 生命周期和作用域

不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题

  • SqlSessionFactoryBuilder

    • 一旦创建了 SqlSessionFactory,就不再需要它了
    • 最佳作用域是方法作用域
  • SqlSessionFactory

    • SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在
    • 没有任何理由丢弃它或重新创建另一个实例
    • 它维护一个连接池,供SqlSession获取连接
  • SqlSession

    • 每个线程都应该有它自己的 SqlSession 实例
    • SqlSession 的实例不是线程安全的,因此是不能被共享的
    • 最佳的作用域是请求或方法作用域,即使用完就关闭

4.8 结果映射

结果集可以使用一个map接收,也可以使用JavaBean或者POJO接收,之前的例子都是使用POJO接收。

使用map接收:

<select id="getStudents" resultType="map">
  select id, username, hashedPassword from students
</select>

使用Pojo接收:

<select id="getStudents" resultMap="students">
    select * from students
</select>

问题:使用Pojo接收时,数据库列名与对象类的属性名不一致怎么办?如下 “name”与“sName”

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBVqw4pF-1613116023827)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208160905129.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8J0m73ly-1613116023829)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208160927986.png)]

解决:使用结果映射

<!--创建一个resultMap结果映射-->
<resultMap id="myMap" type="students">
    <!--colume代表数据库列名,property代表要映射到的类的属性名-->
    <result property="sName" column="name"/>
    <!--未明确标出的映射,会自动安装列名与属性名相同映射-->
</resultMap>

<select id="getStudents" resultMap="myMap">
    select * from students
</select>

结论:

  • resultMap 元素是 MyBatis 中最重要最强大的元素。
  • 它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来
  • ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
  • 如果这个世界总是这么简单就好了。对于复杂的SQL语句,就没那么简单了,在后面继续介绍。

5 日志

5.1 日志工厂STDOUT_LOGGING

在核心配置文件中配置setting即可,无需外加载jar包

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

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZpbesY3-1613116023833)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208163732473.png)]

5.2 log4j

log4J是java常用日志工具,全称是log for java

介绍:

  • 通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器等
  • 可以控制每一条日志的输出格式
  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
  • 最重要的是,可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

配置:

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

需要引入jar包,maven引入

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

控制日志的配置文件

#将等级为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/zl.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

MyBatis使用输出示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zl2jrHvV-1613116023835)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208170633107.png)]

自用使用示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zB3ZyCnO-1613116023837)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210208171225481.png)]

6 分页

为什么使用分页

  • 分页有利于提取少量数据,提高响应速度
  • 减少系统负担

6.1 使用Limit分页

数据库分页的本质都是包含limit的SQL语句分页。

mybatis分页示例:

<select id="getStudentsLimit" parameterType="map" resultType="students">
    select * from `students` limit #{start},#{size}
</select>

测试:

@org.junit.Test
    public void testPage(){
    try (SqlSession session = Mybatis_utils.getSqlSession()){
        StudentsMapper mapper = session.getMapper(StudentsMapper.class);

        int start = 0;
        int size = 4;
        int page = 1;

        Map<String,Integer> map = new HashMap<>();
        map.put("start", start);
        map.put("size",size);

        List<Students> list = mapper.getStudentsLimit(map);
        while(list.size() > 0) {
            showList(list);
            System.out.println(page + " page");

            start = page * size;
            page++;

            map.put("start", start);
            list = mapper.getStudentsLimit(map);
        }

    }
}

注意点:如果使用传两个参数(基本类型和String)的方法,需要使用@Param注解,参数是列名。

import org.apache.ibatis.annotations.Param;
selectAlbum(@Param("userid") Integer userid, @Param("albumName") String albumName);

6.2 RowBonds分页

以前的代码使用的比较多,目前较少使用,看到老代码不慌!

6.3 分页插件

比较流行的PageHelper

7 使用注解

提一句:面向接口编程,接口是定义一系列的行为,是对象与对象交互的协议,面向接口编程最大的好处就是解耦合,从而带来可扩展性、可维护性、分层开发、分模块开发等。

7.1 注解示例

首先,在方法上定义sql语句的注解

public interface UserMapper {

    @Select("select * from user")
    List<User> getUsers();
}

因为没有xml文件了,所以在核心配置文件中,不能通过映射mapper文件来绑定,可以通过之前介绍的后两种方法:指定类名和指定包

<mappers>
    <!--指定类名-->
    <mapper class="com.zl.dao.UserMapper"/>
</mappers>

测试

@Test
public void annotationTest(){
    try(SqlSession session = Mybatis_utils.getSqlSession()){
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> users = mapper.getUsers();

        for (User user : users) {
            System.out.println(user);
        }
    }
}

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

7.2 本质

  • 反射
  • 动态代理

7.3 流程解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nIUtvB2w-1613116023838)(C:\Users\zl\Downloads\1.png)]

7.4 使用注解CRUD

@Select("select * from user")
List<User> getUsers();

@Insert("insert into `user` (`name`,`pwd`) values(#{name}, #{pwd})")
int addUsers(User user);

@Update("update `user` set `name`=#{name} where `id` = #{id}")
int updateUserName(@Param("id") int id,  @Param("name") String name);

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

测试方法和用xml映射是一样的。

#{} ${}的区别:

  • #{}是预编译,${}是字符拼接
  • 使用#{}防止SQL注入,提高系统的安全性。

8 Lombok

Lombok是一种 Java实用工具,可用来帮助开发人员消除Java的冗长,尤其是对于简单的Java对象(POJO), 它通过注释实现这一目的。

安装步骤:

  • IDEA中下载插件
  • 导入Jar包(maven仓库)

Lombok注解

  • @Data:注解在类上,将类提供的所有属性都添加get、set方法,并添加、equals、canEquals、hashCode、toString方法
  • @Setter:注解在类上,为所有属性添加set方法、注解在属性上为该属性提供set方法
  • @Getter:注解在类上,为所有的属性添加get方法、注解在属性上为该属性提供get方法
  • @NotNull:在参数中使用时,如果调用时传了null值,就会抛出空指针异常
  • @Synchronized 用于方法,可以锁定指定的对象,如果不指定,则默认创建一个对象锁定
  • @Log作用于类,创建一个log属性
  • @Builder:使用builder模式创建对象
  • @NoArgsConstructor:创建一个无参构造函数
  • @AllArgsConstructor:创建一个全参构造函数
  • @ToString:创建一个toString方法
  • @Accessors(chain = true)使用链式设置属性,set方法返回的是this对象。
  • @RequiredArgsConstructor:创建对象, 例: 在class上添加@RequiredArgsConstructor(staticName = “of”)会创建生成一个静态方法
  • @UtilityClass:工具类
  • @ExtensionMethod:设置父类
  • @FieldDefaults:设置属性的使用范围,如private、public等,也可以设置属性是否被final修饰。
  • @Cleanup: 关闭流、连接点。
  • @EqualsAndHashCode:重写equals和hashcode方法。
  • @Cleanup: 用于流等可以不需要关闭使用流对象.

使用:

import lombok.Data;

//将类提供的所有属性都添加get、set方法,并添加、equals、canEquals、hashCode、toString方法
@Data
public class User {
    private int id;
    private String name;
    private String pwd;
    private String phone;
}

Lombok的争议:

Lombok为java代码的精简提供了一种方式,但是**所有的源代码很多时候是用来阅读的,只有很少的时间是用来执行的。**Lombok会降低代码的可读性,而且对于升级和扩展带来了一定潜在难度。

9 关联查询

当一个表需要同时关联另一个表查询时,需要用到association关键字。

比如,查询一个学生表,附带其中个的老师信息,学生表的属性是(id,name,tid),tid指向老师表的id。

create table students
(
    id   int auto_increment comment 'ID' primary key,
    name varchar(100) not null comment '姓名',
    tid  int          not null
)

create table teacher
(
    id   int auto_increment primary key,
    name varchar(20) not null
)

学生表的POJO

import lombok.Data;

@Data
public class Student {
    private int id;
    private String name;
    private Teacher teacher; //Teacher类
}

有两种方式查询,一种是连接查询,一种是子查询

<!--连接查询-->
<resultMap id="sMap" type="student">
    <!--property是student类属性名,column代表查询出的表的列名-->
    <result column="sname" property="name"/>
    <result column="sid" property="id"/>
    <!--javaType需要映射到的类-->
    <association property="teacher" javaType="Teacher">
        <!--这里表示的是JavaType的映射-->
        <result column="tid" property="id"/>
        <result column="tname" property="name"/>
    </association>
</resultMap>

<select id="getStudents2" resultMap="sMap">
    select s.id sid, s.name sname, t.id tid, t.name tname
    from `students` as s
    left join `teacher` as t
    on s.tid = t.id
</select>

<!--子查询-->
<resultMap id="stMap" type="student">
    <result column="id" property="id"/>
    <result column="name" property="name"/>
     <!--select代表子查询,查询的结果映射到javaType中,column代表传递给子查询的参数-->
    <association property="teacher" column="tid" javaType="Teacher" select="getTeacherById"/>
</resultMap>

<select id="getStudents" resultMap="stMap">
    select * from `students`
</select>

<select id="getTeacherById" resultType="Teacher">
    select * from `teacher` where `id` = #{id}
</select>

结果:

Student(id=3, name=李四, teacher=Teacher(id=1, name=Janey))
Student(id=5, name=Tom, teacher=Teacher(id=2, name=wen))
Student(id=24, name=zhangsan, teacher=Teacher(id=1, name=Janey))
Student(id=25, name=lisi, teacher=Teacher(id=2, name=wen))

10 集合查询

当一个pojo类型中有一个集合时,需要通过collection关键字查询。

比如,老师表中有一个学生集合

import lombok.Data;

import java.util.List;

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

    private List<Student> studentList; //学生集合
}

同样,有两种查询方式,一种是连接查询,一种是子查询。相对来说,连接查询时推荐的方式,更接近SQL查询的思想,更易于理解和排除错误。

<!--连接查询-->
<resultMap id="map1" type="Teacher">
    <!--id与result一样,只是id用于标记出ID字段,有利于性能提高-->
    <id property="id" column="tid"/>
    <result property="name" column="tname"/>
    <!--ofType代表集合中的元素类型-->
    <collection property="studentList" ofType="student">
        <!--这里表示元素中的每一个对象-->
        <id property="id" column="sid"/>
        <result property="name" column="sname"/>
    </collection>
</resultMap>

<select id="getTeachers" resultMap="map1">
    select t.id tid, t.name tname, s.id sid, s.name sname
    from `teacher` as t
    left join `students` as s
    on t.id = s.tid
</select>

<!--子查询-->
<resultMap id="map2" type="teacher">
    <!--通过select查询获取javaType类型,传参是column-->
    <collection property="studentList" ofType="student" javaType="ArrayList" select="getSudentsbyTid" column="id"/>
</resultMap>

<select id="getTeachers2" resultMap="map2">
    select * from `teacher`
</select>

<select id="getSudentsbyTid" resultType="student">
    select * from `students` where `tid` = #{tid}
</select>

建议: 最好逐步建立结果映射。从最简单的形态开始,逐步迭代。每次都使用单元测试保证每次迭代的正确性。

数据库面试高频:

  • MySQL引擎
  • InnoDB底层
  • 索引
  • 索引优化

11 动态SQL

动态 SQL 是 MyBatis 的强大特性之一。

动态SQL就是根据不同条件拼接SQL语句,MyBatis提供一套标签在xml映射文件中使用的条件判断。

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

11.1 if语句

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。

<!--parameterType传递一个条件类-->
 <select id="getUsers" parameterType="UserCondition" resultType="user">
     select * from `user`
     <!--were标签会根据需要去掉and或者or,或者去掉where-->
     <where>
         <if test="name != null">
             and `name` = #{name}
         </if>
         <if test="hasPhone == true">
             and ( `phone` is not null and `phone` != "")
         </if>
     </where>
</select>

以上可以匹配getUser的两个重载方法:

public interface UserMapper {
    List<User> getUsers();
    List<User> getUsers(UserCondition user);
}

11.2 choose

choose类似于java中的switch语句,只有一个case能够执行,配合when、otherwise标签,每个when相当于一个case,otherwise相当于default。

<select id="getUsers2" parameterType="UserCondition" resultType="User">
    select * from `user`
    <choose>
        <when test="name != null">
            where `name` = #{name}
        </when>
        <when test="hasPhone == true">
            where ( `phone` is not null and `phone` != "")
        </when>
        <otherwise>

        </otherwise>
    </choose>
</select>

11.3 trim

trim可以用于自定义元素的功能,如内置的where,如下所示(注意:此例中的空格是必须的)。

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

比如if例子中,最后拼接的是 select * from user where and name = ?,此处就会把“and ”去掉,如果有的话。

内置的set是如下设置,此处是在结尾删除多余的“,”

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

更新语句示例:

<update id="updateUser" parameterType="UserCondition">
    update `user`
    <set>
        <if test="name != null">
            `name` = #{name},
        </if>
        <if test="phone != null">
            `phone` = #{phone}
        </if>
    </set>
    where `id` = #{id}
</update>

上面也可以写成:

<update id="updateUser" parameterType="UserCondition">
    update `user`
    <!--如果trim内部条件都不满足,什么都不返回。
		整个trim以prefix开头,以suffix结尾,中间拼接的部分的最后去掉“,”-->
    <trim prefix="set" suffix=" where id = #{id} " suffixOverrides=",">

        <if test="name != null"> `name` = #{name} , </if>

        <if test="phone != null"> `phone` = #{phone} , </if>

    </trim>
</update>

翻译:如果trim内部条件都不满足,什么都不返回。如果存在满足条件,整个trim以prefix开头,以suffix结尾,中间拼接的部分的最后去掉“,”

11.4 SQL片段

可以通过sql标签将sql代码片段提取出来,减少重复SQL语句,提高复用。

<sql id="if-name-phone">
    <if test="name != null"> `name` = #{name} , </if>
    <if test="phone != null"> `phone` = #{phone} , </if>
</sql>

<update id="updateUser" parameterType="UserCondition">
    update `user`
    <trim prefix="set" suffix=" where id = #{id} " suffixOverrides=",">
        <!--直接引用sql片段-->
        <include refid="if-name-phone"/>
    </trim>
</update>

一般只将简单的if语句提取出来,连表查询部分,复用性不高,一般不提取,where、set标签等一般也不提取。

11.5 foreach

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

<!--拼接select * from `user` where id in (1, 2, 3, 4)-->
<select id="getUsersForeach" parameterType="ArrayList" resultType="User">
    select * from `user`
    <where>
        <if test="ids.size() > 0">
            <!--collection集合名、open代表拼接的开头,separator为每一项之间的分隔符,close是结尾字符,item代表每一项的变量名-->
            <foreach collection="ids" open="id in (" separator="," close=")" item="id">
                #{id}
            </foreach>
        </if>
    </where>
</select>

11.6 总结

动态SQL的本质就是拼接字符串,字符串最后会形成一个SQL语句,所以在写动态SQL时,首先应该写出完整的SQL语句,并在数据库中执行成功后,再写Mybatis的拼接语句。

12 缓存

12.1 简介

what: 什么是缓存?

  • 存在缓存中的数据
  • 数据库一般是存在读取速度较慢的磁盘中,而将经常查询的数据放在内存中,从而提高查询效率,解决高并发系统的性能问题。

why:为什么使用缓存?

  • 减少与数据库的交互,提升访问速度,提升系统效率。

when:什么时候使用缓存?

  • 当经常查询的数据经常保持不变时,应该使用缓存

12.2 主从复制、读写分离

简单的来说,就是有两个或两个以上的数据库,其中一个当做主数据库,其他当做从数据库,主数据库负责更新数据,从数据库负责查询,当主数据库更新时,会更新到所有从数据库。就实现了主从复制、读写分离的设计。

优点:

  • 当其中一个数据库出问题时,其他数据库可以工作,保证数据不易丢失,且稳定工作
  • 写操作更耗时,将读写分离,更新操作不会影响到查询操作。

12.3 Mybatis缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。

MyBatis定义了两级缓存:会话缓存和二级缓存

  • 默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。(在一个SqlSession中缓存,调用close即缓存结束)
  • 二级缓存需要手动开启和配置,在映射文件中使用标签,作用域是所在namespace内。
  • 二级缓存也可以扩展Cache接口定义

12.4 一级缓存

  • 一级缓存是默认开启的
  • 作用域是获取SQLSession到close关闭
  • 期间的查询会被缓存
  • 期间所有的更新操作(insert、delete、update)会清除缓存
  • 默认采用LRU(最近最少使用算法)
  • 采用读/写缓存,即缓存不是共享的,可以安全的被调用者修改。

通过Log4j日志输出,可以查看到select语句只执行了一次,如果中间有更新操作,select会再次执行。

[com.zl.dao.UserMapper.getUsers]-==>  Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters: 
[com.zl.dao.UserMapper.getUsers]-<==      Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
======================
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
======================
[com.zl.dao.UserMapper.updateUser]-==>  Preparing: update `user` set `phone` = ? where id = ?
[com.zl.dao.UserMapper.updateUser]-==> Parameters: 5698(String), 4(Integer)
[com.zl.dao.UserMapper.updateUser]-<==    Updates: 1
[com.zl.dao.UserMapper.getUsers]-==>  Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters: 
[com.zl.dao.UserMapper.getUsers]-<==      Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=5698)
User(id=5, name=Sam, pwd=789, phone=5566)

12.5 二级缓存

一级缓存的作用域太小,诞生了二级缓存,二级缓存的作用域是基于namespace,只需要加入标签即可,当然,在核心配置文件中可以增加setting设置,显视的表示开启全局缓存,虽然默认是开启的。

首先查询保存在一级缓存中,当close关闭时,会将一级缓存保存到二级缓存,使用了java对象序列化。在另一个连接查询中,读取缓存时,会从一级缓存和二级缓存中读取。

<mapper namespace="com.zl.dao.UserMapper">

    <!--开启二级缓存,标签内也可以进行详细配置-->
    <cache/>

    <select id="getUsers" parameterType="UserCondition" resultType="user">
        select * from `user`
        <where>
            <if test="name != null">
                and `name` = #{name}
            </if>
            <if test="hasPhone == true">
                and ( `phone` is not null and `phone` != "")
            </if>
        </where>
    </select>
</mapper>
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

注意:二级缓存使用了序列化,所以类必须可以序列化

测试:

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

            System.out.println("=====================");
        }

        try(SqlSession session1 = Mybatis_utils.getSqlSession()){
            UserMapper mapper = session1.getMapper(UserMapper.class);
            List<User> users = mapper.getUsers();
            for (User user : users) {
                System.out.println(user);
            }

            System.out.println("=====================");
        }


    }

下面结果显示,两次获取连接,只查询了一次 select * from user

[com.zl.dao.UserMapper]-Cache Hit Ratio [com.zl.dao.UserMapper]: 0.0
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 1912850431.
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[com.zl.dao.UserMapper.getUsers]-==>  Preparing: select * from `user`
[com.zl.dao.UserMapper.getUsers]-==> Parameters: 
[com.zl.dao.UserMapper.getUsers]-<==      Total: 3
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
=====================
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7203c7ff]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1912850431 to pool.
[org.apache.ibatis.io.SerialFilterChecker]-As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
[com.zl.dao.UserMapper]-Cache Hit Ratio [com.zl.dao.UserMapper]: 0.5
User(id=1, name=Tom, pwd=123, phone=null)
User(id=4, name=Janey, pwd=456, phone=1234567890)
User(id=5, name=Sam, pwd=789, phone=5566)
=====================

12.6 缓存原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9pXoP1Xr-1613116023840)(C:\Users\zl\Downloads\未命名文件.png)]

12.7 自定义缓存

可以在引用自定义缓存。

步骤就是在java中继承cache类,然后设置在标签中。

<cache type="com.domain.something.MyCustomCache"/>

自定义cache需要实现的cache接口,不过目前有大量开源的数据库缓存框架,你确定能做的比他们好,再自定义缓存吧,-_-||

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

也可以引入开源的缓存框架,如ehcache(需要导包,具体查看网上)

<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

不过,一般会使用redies数据库做缓存

上一篇:SQL命令


下一篇:自动化测试常用脚本-获取日期加减之后的日期