23:MyBatis详解 || 接下来的每一步前行都是求职之旅

MyBatis 详解

环境:

  • JDK:1.8
  • MySQL:8.0 / 5.7
  • Maven:3.5.3
  • IDEA

回顾:

  • JDBC
  • MySQL
  • Java基础
  • Maven
  • Junit

框架学习:都是有配置文件的,最好的学习方式就是看官网的文档!!!

SSM:Spring、SpringMVC、MyBatis。

MyBatis官方文档:https://mybatis.net.cn/


一、简介

1. 什么是MyBatis

image-20220428161606151

  • MyBatis 是一款优秀的持久层框架
  • 它支持自定义 SQL、存储过程以及高级映射。
  • MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
  • MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
  • MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了[google code](https://baike.baidu.com/item/google code/2346604),并且改名为MyBatis。
  • 2013年11月迁移到Github。
  • iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)。

如何获得MyBatis?

2. 持久化

数据持久化

  • 持久化就是将程序的数据在持久状态(放在数据库中只要不删库就是持久的)和瞬时状态转化的过程
  • 内存特性:断电即失,数据在内存中容易丢失,所以需要持久化
  • IO文件持久化(特别浪费资源)--> 数据库诞生(使用JDBC)
  • 生活中的持久化:冷藏和解冻

为什么需要持久化?

  • 有一些对象不能让它丢掉,比如账号里的钱等等
  • 内存很贵

3. 持久层

Dao层、Service层、Controller层...

持久层:

  • 完成持久化工作的代码块
  • 层的界限是十分明显的

4. 为什么需要MyBatis

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

二、第一个MyBatis程序

思路:搭建环境 --> 导入MyBatis --> 编写代码 --> 测试!

1. 搭建环境

搭建数据库:

CREATE DATABASE `mybatis_study`;

USE `mybatis_study`;

CREATE TABLE `user` (
	`id` INT (20) NOT NULL,
	`name` VARCHAR (30) DEFAULT NULL,
	`pwd` VARCHAR (30) DEFAULT NULL,
	PRIMARY KEY (id)
) ENGINE = INNODB DEFAULT CHARSET = utf8;

INSERT INTO `user` (`id`, `name`, `pwd`) VALUES
(1,'Laxsilence','123456'),
(2,'Liu','123456'),
(3,'Dear','123456');

新建项目:

  1. 新建一个普通的Maven项目

  2. 删除src目录,将该项目当做父工程,后续所有的项目都用module建子工程

    在pom中标记父工程:

    image-20220428165757381

  3. 导入Maven依赖

    pom.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
    <!--    父工程-->
        <groupId>com.laxsilence</groupId>
        <artifactId>MyBatis-Study</artifactId>
        <packaging>pom</packaging>
        <version>1.0-SNAPSHOT</version>
        <modules>
            <module>mybatis-01</module>
        </modules>
        <!--    导入依赖-->
        <dependencies>
    <!--        mysql 驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.16</version>
            </dependency>
    <!--        mybatis-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.6</version>
            </dependency>
    <!--        junit-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.1</version>
                <scope>test</scope>
            </dependency>
    <!--        lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.16</version>
            </dependency>
        </dependencies>
    
        <!--在build中配置resources,来防止我们资源导出失败的问题,最好是在子项目中也放一份-->
        <build>
            <resources>
                <resource>
                    <directory>src/main/resources</directory>
                    <includes>
                        <include>**/*.properties</include>
                        <include>**/*.xml</include>
                    </includes>
                    <filtering>true</filtering>
                </resource>
                <resource>
                    <directory>src/main/java</directory>
                    <includes>
                        <include>**/*.properties</include>
                        <include>**/*.xml</include>
                    </includes>
                    <filtering>false</filtering>
                </resource>
            </resources>
        </build>
    
    </project>
    

2. 创建一个模块

new一个module,同样选择普通的maven项目

可以发现新模块中:不用添加依赖,可以直接用父项目中添加的依赖。

  1. 编写Mybatis的核心配置文件

    XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例:

    注意:配置文件中不要写中文注释!!!以下代码中的中文注释只是用来讲解,运行时记得删掉

    <?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 核心配置文件-->
    <configuration>
       <!--设置默认环境-->
      <environments default="development">
          <!--可以有多个环境-->
        <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/mybatis_study?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT"/>
                    <property name="username" value="root"/>
                    <property name="password" value="19142001lj"/>
          </dataSource>
        </environment>
      </environments>
    <!--    每一个Mapper.xml都需要在mybatis-config.xml这个核心 配置文件中进行注册-->
      <mappers>
          <!--这里需要注意,路径是/不是.  类用. 文件用/ -->
          <mapper resource="com/laxsilence/dao/UserMapper.xml"></mapper>
      </mappers>   
    </configuration>
    

    当然,还有很多可以在 XML 文件中配置的选项,上面的示例仅罗列了最关键的部分。 注意 XML 头部的声明,它用来验证 XML 文档的正确性。environment 元素体中包含了事务管理和连接池的配置。mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。

  2. 编写Mybatis工具类

    为什么需要工具类,因为多次读取配置文件,会造成资源的浪费

    每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

    从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。

    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    

    从 SqlSessionFactory 中获取 SqlSession

    既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

    try (SqlSession session = sqlSessionFactory.openSession()) {
    Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
    }
    

    诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。

    例如:

    try (SqlSession session = sqlSessionFactory.openSession()) {
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    Blog blog = mapper.selectBlog(101);
    }
    

    实操代码整合:

    package com.laxsilence.utils;
    
    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 java.io.IOException;
    import java.io.InputStream;
    
    /**
     * @ClassName MyBatisUtils
     * @Description TODO
     * @Author 刘英俊
     * @Date 2022/4/28 17:26
     * @Version 1.0
     **/
    //工厂设计模式
    //sqlSessionFactory  -- > 用来生产sqlSession (类似于jdbc中的prepareStatement,用来执行sql语句)
    public class MyBatisUtils {
    
        private static SqlSessionFactory sqlSessionFactory;
    
        static {
            try {
                // 使用MyBatis第一步:  获取 sqlSessionFactory 对象
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    //    既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
    //    SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句
        public static SqlSession getSqlSession(){
           return sqlSessionFactory.openSession();
        }
    }
    

3. 编写代码

  • 实体类,以后所有的实体类注意实现序列化

    package com.laxsilence.pojo;
    
    import java.io.Serializable;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
        /**
         *
         */
        private Integer id;
    
        /**
         *
         */
        private String name;
    
        /**
         *
         */
        private String pwd;
    
        private static final long serialVersionUID = 1L;
    }
    
  • Dao接口

    package com.laxsilence.dao;
    
    import com.laxsilence.pojo.User;
    
    import java.util.List;
    
    //Dao等价于Mapper,以后慢慢用Mapper
    public interface UserDao {
        List<User> getUserList();
    }
    

    注意补充:以前我们需要实现接口中的JDBC方法:

    package com.laxsilence.dao;
    
    import com.laxsilence.pojo.User;
    
    import java.util.List;
    
    /**
     * @ClassName UserDaoImpl
     * @Description TODO
     * @Author 刘英俊
     * @Date 2022/4/28 17:43
     * @Version 1.0
     **/
    public class UserDaoImpl implements UserDao{
    //    以前在这里写JDBC的代码,但是现在不需要了,我们只需要建立接口的配置文件
        public List<User> getUserList() {
            return null;
        }
    }
    

    但是现在不需要了,我们只需要建立接口的映射配置文件 UserMapper.xml!

    探究已映射的 SQL 语句

    现在你可能很想知道 SqlSession 和 Mapper 到底具体执行了些什么操作,但 SQL 语句映射是个相当广泛的话题,可能会占去文档的大部分篇幅。 但为了让你能够了解个大概,这里会给出几个例子。

    在上面提到的例子中,一个语句既可以通过 XML 定义,也可以通过注解定义。我们先看看 XML 定义语句的方式,事实上 MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现,这使得 MyBatis 在过去的数年间得以流行。如果你用过旧版本的 MyBatis,你应该对这个概念比较熟悉。 但相比于之前的版本,新版本改进了许多 XML 的配置,后面我们会提到这些改进。这里给出一个基于 XML 映射语句的示例,它应该可以满足上个示例中 SqlSession 的调用。

    <?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="org.mybatis.example.BlogMapper">
    <select id="selectBlog" resultType="Blog">
     select * from Blog where id = #{id}
    </select>
    </mapper>
    

    为了这个简单的例子,我们似乎写了不少配置,但其实并不多。在一个 XML 映射文件中,可以定义无数个映射语句,这样一来,XML 头部和文档类型声明部分就显得微不足道了。文档的其它部分很直白,容易理解。 它在命名空间 “org.mybatis.example.BlogMapper” 中定义了一个名为 “selectBlog” 的映射语句,这样你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 来调用映射语句了,就像上面例子中那样:

    Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
    

    你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法,就像下面这样:

    BlogMapper mapper = session.getMapper(BlogMapper.class);
    Blog blog = mapper.selectBlog(101);
    

    第二种方法有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句。

    提示 对命名空间的一点补充

    在之前版本的 MyBatis 中,命名空间(Namespaces)的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。

    命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。

    命名解析:为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。

    • 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
    • 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。

    对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

    package org.mybatis.example;
    public interface BlogMapper {
      @Select("SELECT * FROM blog WHERE id = #{id}")
      Blog selectBlog(int id);
    }
    

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

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

  • 接口实现类(接口映射),由原来的UserDaoImpl转换为了UserMapper.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">
    <!--几乎所有的配置文件 都可以在尖括号中通过空格进行查看配合-->
    <!--命名空间:namespace= 绑定一个对应的Dao/Mapper接口,让SQL跟接口产生交集,实现原理类似于 新建实现类 实现 implement 接口-->
    <mapper namespace="com.laxsilence.dao.UserDao">
        <!-- select 查询语句 -->
        <!-- id : 对应接口中方法的名字  类似于  实现类中重写接口中的方法-->
        <!-- resultType:返回结果集的类型,要写全限定名,需要注意,这里需要写集合泛型中的类型,而不是写List-->
        <select id="getUserList" resultType="com.laxsilence.pojo.User">
            <!--sql语句-->
            select * from user
        </select>
    </mapper>
    

4. 测试(使用JUnit)

按照Maven的标准,写在test包中:

image-20220428180422853

同时注意:测试的包跟实际的java包尽量做到一一对应,这是一种比较好的规范

测试代码:

package com.laxsilence.dao;

import com.laxsilence.pojo.User;
import com.laxsilence.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

/**
 * @ClassName UserDaoTest
 * @Description TODO
 * @Author 刘英俊
 * @Date 2022/4/28 18:06
 * @Version 1.0
 **/
public class UserDaoTest {
    @Test
    public void test(){
        //以前需要获得preparedStatement去执行,现在我们需要获取前面工具类中写好的sqlSession,他两其实是差不多的,只不过换了一个名字

        //第一步:获得SqlSession对象
        SqlSession sqlSession = MyBatisUtils.getSqlSession();

        //第二步:执行SQL,从sqlSession中获取mapper,也就是dao,后期会把Dao接口名字写成Mapper接口这样
        //我们需要获取到一个UserDao类的对象才能够通过.获得它的方法,通过反射.class获取类的一个实例对象
        //类似于 UserDao userDao = new UserDaoImpl();
        //方式1:getMapper
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        List<User> userList = userDao.getUserList();
        for (User user : userList) {
            System.out.println(user.toString());
        }
        //方式2:不推荐,需要根据方法的返回值类型来决定自己sqlSeesion的方法
        List<User> userList1 = sqlSession.selectList("com.laxsilence.dao.UserDao.gerUserList");
        for (User user : userList1) {
            System.out.println(user.toString());
        }

        //关闭SqlSession
        sqlSession.close();
    }
}

注意点:

错误1:org.apache.ibatis.binding.BindingException:Type interface com.laxsilence.dao.UserDao is not known to the MapperRegistry

  • 就是说:在MapperRegistry核心注册文件中没有找导UserDao

  • 在mybatis-config.xml这个核心配置文件中有这样一些配置:

    <mappers>
        <mapper resource="org/mybatis/example/BlogMapper.xml"/>
    </mappers>
    
  • 可以发现,这里需要一个我们自己的路径

    <!--    每一个Mapper.xml都需要在mybatis-config.xml这个核心 配置文件中进行注册-->
        <mappers>
            <!--这里需要注意,路径是/不是.  类用. 文件用/ -->
            <mapper resource="com/laxsilence/dap/UserMapper.xml"></mapper>
        </mappers>
    

错误2:the error may exist in com/laxsilence/dap/UserMapper.xml

  • 初始化异常,找不到这个文件,那我们已经绑定了,为什么找不到呢

  • 这里就是Maven中我们提到的资源过滤问题,因为Maven是约定大于配置的,所以写在java包中的资源,在打包后的target中是找不到的,我们可以将这个文件拷贝到target中的dao包下,但每一次都拷贝不是一个好的解决方案

    image-20220428183318696

  • 解决:在pom.xml中加入以下内容:

    <!--在build中配置resources,来防止我们资源导出失败的问题-->
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
    

错误3:org.apache.ibatis.builder.BuilderException: Error creating document instance. Cause: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: 1 字节的 UTF-8 序列的字节 1 无效。

  • 这里就是上边我们说的中文注释的问题

  • 解决方:1:去掉xml中的中文注释即可,或者将头文件中的UTF-8改成UTF8

  • 解决方法2:在idea的File-Settings-File Encodings中将编码统统设置为 UTF-8 即可(该方法需要重启IDEA)

    image-20220428184659056

测试结果:

image-20220428185040411

需要避免的问题:

  1. 配置文件没有注册
  2. 绑定接口错误
  3. 方法名不对
  4. 返回类型不对
  5. Maven导出资源问题

三、CRUD 增删改查

从这开始,我们的UserDao.java 彻底改名为UserMapper.java

增删改查标签中的几个常用属性:

  • id:就是对应的namespace中的方法名
  • resultType:sql语句执行的返回值类型
  • parameterType:参数类型,parameterType的值就是接口的形式参数的类型

增删改查的步骤都是一样的

  1. 编写接口
  2. 编写对应的Mapper.xml中的sql语句
  3. 测试

注意点:增删改需要提交事务!

1. namespace

UserMapper.xml中namespace中的接口名要和Dao或Mapper的接口名完全一致,包括包名!

2. Select

选择查询语句;

  1. 编写接口

    //查询全部用户
    List<User> getUserList();
    //根据ID查询用户
    User getUserById(int id);
    
  2. 编写对应的Mapper.xml中的sql语句

    <!--查询所有数据-->
    <select id="getUserList" resultType="com.laxsilence.pojo.User">
        select * from user
    </select>
    
    <!-- 注意parameterType的值就是接口的形式参数的类型-->
    <select id="getUserById" parameterType="int" resultType="com.laxsilence.pojo.User">
        <!--注意传进去的参数用#{} 括号中的值就是接口中的形式参数的名字-->
        select * from user where id = #{id}
    </select>
    
  3. 测试

    //查询所有用户
    @Test
    public void test(){
        //以前需要获得preparedStatement去执行,现在我们需要获取前面工具类中写好的sqlSession,他两其实是差不多的,只不过换了一个名字
        SqlSession sqlSession = null;
        //根据官网的标准,以下代码最好写在try中
        try {
            //第一步:获得SqlSession对象
            sqlSession = MyBatisUtils.getSqlSession();
            //第二步:执行SQL,从sqlSession中获取mapper,也就是dao,后期会把Dao接口名字写成Mapper接口这样
            //我们需要获取到一个UserDao类的对象才能够通过.获得它的方法,通过反射.class获取类的一个实例对象
            //类似于 UserDao userDao = new UserDaoImpl();
            //方式1:getMapper
            UserMapper userDao = sqlSession.getMapper(UserMapper.class);
            List<User> userList = userDao.getUserList();
            for (User user : userList) {
                System.out.println(user.toString());
            }
            //方式2:不推荐,需要根据方法的返回值类型来决定自己sqlSeesion的方法
            List<User> userList1 = sqlSession.selectList("com.laxsilence.dao.UserDao.gerUserList");
            for (User user : userList1) {
                System.out.println(user.toString());
            }
        } finally {
            //关闭SqlSession
            sqlSession.close();
        }
    }
    
    //根据ID查询用户
    @Test
    public void getUserById(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.getUserById(1);
        System.out.println(user.toString());
        sqlSession.close();
    }
    

3. Insert

添加语句

  1. 编写接口

        //插入一个用户
        int addUser(User user);
    
  2. 编写对应的Mapper.xml中的sql语句

     <!--插入用户-->
        <!--对象中的属性可以直接取出来-->
        <insert id="addUser" parameterType="com.laxsilence.pojo.User">
            insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
        </insert>
    
  3. 测试

     //添加用户
        //增删改需要提交事务,以前之所以不写提交是因为开启了自动提交,mybatis增删改都需要手动提交
        @Test
        public void addUser(){
            SqlSession sqlSession = MyBatisUtils.getSqlSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            int res = mapper.addUser(new User(4, "英俊", "123456"));
            if (res>0){
                System.out.println("插入成功!");
            }
            //提交事务
            sqlSession.commit();
            sqlSession.close();
        }
    

4. Update

更新修改语句;

  1. 编写接口

        //修改用户
        int updateUser(User user);
    
  2. 编写对应的Mapper.xml中的sql语句

     <update id="updateUser" parameterType="com.laxsilence.pojo.User">
            update user set name=#{name},pwd=#{pwd} where id=#{id};
        </update>
    
  3. 测试

        //修改用户
        @Test
        public void updateUser(){
            SqlSession sqlSession = MyBatisUtils.getSqlSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            mapper.updateUser(new User(2,"Hello","000000"));
            sqlSession.commit();
            sqlSession.close();
        }
    

5. Delete

删除语句;

  1. 编写接口

        //删除用户
        int deleteUser(int id);
    
  2. 编写对应的Mapper.xml中的sql语句

        <delete id="deleteUser" parameterType="int">
            delete from user where id = #{id};
        </delete>
    
  3. 测试

        //删除用户
        @Test
        public void deleteUser(){
            SqlSession sqlSession = MyBatisUtils.getSqlSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            mapper.deleteUser(4);
            sqlSession.commit();
            sqlSession.close();
        }
    

6. 分析错误

  • 标签不要匹配错,增删改查语句对应着增删改查的标签
  • 核心配置文件中绑定Mapper.xml的时候,注意路径,用/不是.
  • 程序配置文件符合规范不能错
  • 空指针异常自行解决,可能是同一个变量定义了两次
  • 输出的xml文件中存在中文乱码问题
  • maven资源没有导出问题(已经提过多次)

7. 万能的Map

假设我们实体类或者数据库中的表,字段或者参数过多,我们应当考虑使用Map!

接口类:

//万能的Map测试
int addUser2(Map<String,Object> map);

UserMapper.xml:

<!--传递map的key进去,在测试的时候给key对应的value设为前端传进来的值-->
<!--使用map的优点:比如在增加或修改的时候就不需要传递完整的User对象进去了,那样要写的参数太多,使用map只需要写有用的参数即可-->
<!--比如说:要修改id为3的用户的密码,只需要传递id和password就够了,都不需要姓名-->
<insert id="addUser2" parameterType="map">
    insert into user(id,pwd) values (#{userid},#{passWord})
</insert>

测试类:

//测试map
@Test
public void addUser2(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("userid",5);
    map.put("passWord","222222");
    mapper.addUser2(map);
    sqlSession.commit();
    sqlSession.close();
}

可以发现,插入成功,当然,因为我们的mapper.xml中没有传递姓名参数,所以这里的name为空。

image-20220429144026336

接口类:

//根据ID查询用户
User getUserById2(Map<String,Object> map);

UserMapper.xml:

<select id="getUserById2" parameterType="map" resultType="com.laxsilence.pojo.User">
    select * from user where id = #{userid} and name = #{username};
</select>

测试类:

//根据ID查询用户
@Test
public void getUserById2(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("userid",2);
    map.put("username","Hello");
    User user2 = mapper.getUserById2(map);
    System.out.println(user2.toString());
    sqlSession.close();
}

image-20220429145018312

总结:

Map传递参数,直接在sql中写key即可!【parameterType=“map”】

对象传递参数,直接在sql中写对象的属性即可!【parameterType=“Object”】

只有一个基本参数类型的时候,可以直接在sql中取到形参的名称【parameterType可以省略】

有多个参数的时候,用Map或者注解!

8. 模糊查询(#{} 和 ${} 的区别)SQL注入

需要预防SQL注入!

补充:MyBatis的#{}${}的不同

执行可发现二者的SQL执行语句不一样。说明一个是预编译,然后将参数设置进去,安全,没有SQL注入的安全问题

一个是SQL拼接,会有SQL注入问题

img

那是不是${}就没有用处了?

不是。当表名需要动态传参的时候,就只能使用#{}。因为表名不支持预编译,说白了,SQL语句中支持预编译的位置用#{},不支持的位置只能用${}

img

Mapper层

img

XML层

img

模糊查询:

Java代码执行的时候,传递%%通配符:

@Test
public void getUserLikeList(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> userLikeList = mapper.getUserLikeList("%Dear%");
    for (User user : userLikeList) {
        System.out.println(user.toString());
    }
}

在mapper.xml中的sql里拼接%%

<select id="getUserLikeList" parameterType="string" resultType="com.laxsilence.pojo.User">
    select * from user where name like '%'#{value}'%';
</select>

四、配置解析

1. 核心配置文件

  • mybatis-config.xml

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:

2. 环境配置(environments)

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:

  • 每个数据库对应一个 SqlSessionFactory 实例

为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

如果忽略了环境参数,那么将会加载默认环境,如下所示:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);

environments 元素定义了如何配置环境。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意一些关键点:

  • 默认使用的环境 ID(比如:default="development")。
  • 每个 environment 元素定义的环境 ID(比如:id="development")。
  • 事务管理器的配置(比如:type="JDBC")。
  • 数据源的配置(比如:type="POOLED")。

默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。

事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。

  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:

    <transactionManager type="MANAGED">
      <property name="closeConnection" value="false"/>
    </transactionManager>
    

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

这两种事务管理器类型都不需要设置任何属性。它们其实是类型别名,换句话说,你可以用 TransactionFactory 接口实现类的全限定名或类型别名代替它们。

public interface TransactionFactory {
  default void setProperties(Properties props) { // 从 3.5.2 开始,该方法为默认方法
    // 空实现
  }
  Transaction newTransaction(Connection conn);
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

在事务管理器实例化后,所有在 XML 中配置的属性将会被传递给 setProperties() 方法。你的实现还需要创建一个 Transaction 接口的实现类,这个接口也很简单:

public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
  Integer getTimeout() throws SQLException;
}

使用这两个接口,你可以完全自定义 MyBatis 对事务的处理。

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

  • 大多数 MyBatis 应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。

有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:

  • driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
  • url – 这是数据库的 JDBC URL 地址。
  • username – 登录数据库的用户名。
  • password – 登录数据库的密码。
  • defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
  • defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)。查看 java.sql.Connection#setNetworkTimeout() 的 API 文档以获取更多信息。

作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:

  • driver.encoding=UTF8

这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为 UTF8encoding 属性给数据库驱动。

POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。

除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:

  • poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
  • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
  • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
  • poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
  • poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnectionspoolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
  • poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
  • poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
  • poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。

JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:

  • initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
  • data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。

和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:

  • env.encoding=UTF8

这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8encoding 属性。

你可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现:

public interface DataSourceFactory {
  void setProperties(Properties props);
  DataSource getDataSource();
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入 C3P0 数据源所必需的代码:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {

  public C3P0DataSourceFactory() {
    this.dataSource = new ComboPooledDataSource();
  }
}

为了令其工作,记得在配置文件中为每个希望 MyBatis 调用的 setter 方法增加对应的属性。 下面是一个可以连接至 PostgreSQL 数据库的例子:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
</dataSource>

总结:

Mybatis默认的事务管理器就是JDBC;

默认的数据源是:连接池pooled。

3. 属性(properties)

我们可以通过Properties属性来实现引用配置文件!

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。例如:

<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</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>

这个例子中的 username 和 password 将会由 properties 元素中设置的相应值来替换。 driver 和 url 属性将会由 config.properties 文件中对应的值来替换。这样就为配置提供了诸多灵活选择。

也可以在 SqlSessionFactoryBuilder.build() 方法中传入属性值。例如:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);

// ... 或者 ...

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);

如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:

  • 首先读取在 properties 元素体内指定的属性。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。

从 MyBatis 3.4.2 开始,你可以为占位符指定一个默认值。例如:

<dataSource type="POOLED">
  <!-- ... -->
  <property name="username" value="${username:ut_user}"/> <!-- 如果属性 'username' 没有被配置,'username' 属性的值将为 'ut_user' -->
</dataSource>

这个特性默认是关闭的。要启用这个特性,需要添加一个特定的属性来开启这个特性。例如:

<properties resource="org/mybatis/example/config.properties">
  <!-- ... -->
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- 启用默认值特性 -->
</properties>

提示 如果你在属性名中使用了 ":" 字符(如:db:username),或者在 SQL 映射中使用了 OGNL 表达式的三元运算符(如: ${tableName != null ? tableName : 'global_constants'}),就需要设置特定的属性来修改分隔属性名和默认值的字符。例如:

<properties resource="org/mybatis/example/config.properties">
  <!-- ... -->
  <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> <!-- 修改默认值的分隔符 -->
</properties>
<dataSource type="POOLED">
  <!-- ... -->
  <property name="username" value="${db:username?:ut_user}"/>
</dataSource>

实际操作

  1. 编写一个配置文件:db.properties(这就是典型的java属性文件)

    driver:com.mysql.cj.jdbc.Driver
    url:jdbc:mysql://localhost:3306/mybatis_study?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT
    username:root
    password:19142001lj
    
  2. 在核心配置文件中引入

    要求:image-20220429160630042

    <?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>
        <!--引入外部配置文件-->
        <properties resource="db.properties"/>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}"/>
                    <property name="url" value="${url}"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}}"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="com/laxsilence/dao/UserMapper.xml"></mapper>
        </mappers>
    </configuration>
    

注意:

除了引入外部属性文件,在properties标签里也可以直接设置:

<property name="username" value="dev_user"/>

还可以在java类里通过方法参数传入

存在优先级问题:

如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:

  • 首先读取在 properties 元素体内指定的属性。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。

4. 类型别名(typeAliases)

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

<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
<package name="domain.blog"/>
</typeAliases>

每一个在包 domain.blog 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。见下面的例子:

@Alias("author")
public class Author {
 ...
}

下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

实际操作:

  • 类型别名是为Java类型设置一个短的名字,在mapper.xml中不再通过全限定名指定返回结果或参数

方法1:

<typeAliases>
    <typeAlias type="com.laxsilence.pojo.User" alias="User"/>
</typeAliases>

方法2:可以指定一个包名,Mybatis会在包名下面搜索需要的JavaBean,扫描实体类的包,包下的类的默认别名就是类名,首字母小写!

<typeAliases>
    <typeAlias type="com.laxsilence.pojo"/>
</typeAliases>

使用建议:

  • 如果是在实体列比较少的时候,可以使用第一种方式
  • 如果是实体类比较多,建议使用第二种

第一种是可以自定义别名的,第二种是自动生成的(也可以通过注解来自定义:在类上方加上注解--> @Alias(“别名”) ---> Mapper.xml就可以使用这个类的别名了)

优先级:

  • 类型别名 > 注解别名 > 包别名(注解别名 > 默认别名)

5. 设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等。

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 true | false false
aggressiveLazyLoading 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。 true | false false (在 3.4.1 及之前的版本中默认为 true)
multipleResultSetsEnabled 是否允许单个语句返回多结果集(需要数据库驱动支持)。 true | false true
useColumnLabel 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 true | false true
useGeneratedKeys 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 true | false False
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 NONE, PARTIAL, FULL PARTIAL
autoMappingUnknownColumnBehavior 指定发现自动映射目标未知列(或未知属性类型)的行为。NONE: 不做任何反应WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARNFAILING: 映射失败 (抛出 SqlSessionException) NONE, WARNING, FAILING NONE
defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 任意正整数 未设置 (null)
defaultFetchSize 为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 任意正整数 未设置 (null)
defaultResultSetType 指定语句默认的滚动策略。(新增于 3.5.2) FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) 未设置 (null)
safeRowBoundsEnabled 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 true | false False
safeResultHandlerEnabled 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 true | false True
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 true | false False
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 SESSION | STATEMENT SESSION
jdbcTypeForNull 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 OTHER
lazyLoadTriggerMethods 指定对象的哪些方法触发一次延迟加载。 用逗号分隔的方法列表。 equals,clone,hashCode,toString
defaultScriptingLanguage 指定动态 SQL 生成使用的默认脚本语言。 一个类型别名或全限定类名。 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
defaultEnumTypeHandler 指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5) 一个类型别名或全限定类名。 org.apache.ibatis.type.EnumTypeHandler
callSettersOnNulls 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 true | false false
returnInstanceForEmptyRow 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) true | false false
logPrefix 指定 MyBatis 增加到日志名称的前缀。 任何字符串 未设置
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置
proxyFactory 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB | JAVASSIST JAVASSIST (MyBatis 3.3 以上)
vfsImpl 指定 VFS 的实现 自定义 VFS 的实现的类全限定名,以逗号分隔。 未设置
useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) true | false true
configurationFactory 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) 一个类型别名或完全限定类名。 未设置

一个配置完整的 settings 元素的示例如下:

<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

目前需要记住三个:

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 true | false false
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置

6. 映射器(mappers)

映射器我们在第一个Mybatis程序中已经使用过了

一个Dao层,就有一个Mapper文件,就需要去核心配置中绑定这个Mapper文件

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:

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

<!-- 使用完全限定资源定位符(URL) -->
<!-- 完全不推荐-->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/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>

这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,也就是接下来我们要讨论的。

注意:如果将mapper.xml映射文件放到resources目录下,不能使用第三种绑定方法!!!所以建议将映射文件放到Dao包下

  • 方式一:可以随便用,注意后缀.xml结尾

  • 方式二:不要用

  • 方式三:使用class文件绑定注册,是有很多要求的!

    1. 接口和它的Mapper.xml配置文件必须同名
    2. 接口和它的Mapper.xml必须在同一个包下
  • 方式四:使用扫描包注入绑定

    注意:

    1. 接口和它的Mapper.xml配置文件必须同名
    2. 接口和它的Mapper.xml必须在同一个包下

7. 其他(了解即可)

这三个几乎不用,只需要了解即可

插件补充:

  • mybatis-generator-core
  • mybatis-plus
  • 通用Mapper

类型处理器(typeHandlers)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。

提示 从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。

类型处理器 Java 类型 JDBC 类型
BooleanTypeHandler java.lang.Boolean, boolean 数据库兼容的 BOOLEAN
ByteTypeHandler java.lang.Byte, byte 数据库兼容的 NUMERICBYTE
ShortTypeHandler java.lang.Short, short 数据库兼容的 NUMERICSMALLINT
IntegerTypeHandler java.lang.Integer, int 数据库兼容的 NUMERICINTEGER
LongTypeHandler java.lang.Long, long 数据库兼容的 NUMERICBIGINT
FloatTypeHandler java.lang.Float, float 数据库兼容的 NUMERICFLOAT
DoubleTypeHandler java.lang.Double, double 数据库兼容的 NUMERICDOUBLE
BigDecimalTypeHandler java.math.BigDecimal 数据库兼容的 NUMERICDECIMAL
StringTypeHandler java.lang.String CHAR, VARCHAR
ClobReaderTypeHandler java.io.Reader -
ClobTypeHandler java.lang.String CLOB, LONGVARCHAR
NStringTypeHandler java.lang.String NVARCHAR, NCHAR
NClobTypeHandler java.lang.String NCLOB
BlobInputStreamTypeHandler java.io.InputStream -
ByteArrayTypeHandler byte[] 数据库兼容的字节流类型
BlobTypeHandler byte[] BLOB, LONGVARBINARY
DateTypeHandler java.util.Date TIMESTAMP
DateOnlyTypeHandler java.util.Date DATE
TimeOnlyTypeHandler java.util.Date TIME
SqlTimestampTypeHandler java.sql.Timestamp TIMESTAMP
SqlDateTypeHandler java.sql.Date DATE
SqlTimeTypeHandler java.sql.Time TIME
ObjectTypeHandler Any OTHER 或未指定类型
EnumTypeHandler Enumeration Type VARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值)
EnumOrdinalTypeHandler Enumeration Type 任何兼容的 NUMERICDOUBLE 类型,用来存储枚举的序数值(而不是名称)。
SqlxmlTypeHandler java.lang.String SQLXML
InstantTypeHandler java.time.Instant TIMESTAMP
LocalDateTimeTypeHandler java.time.LocalDateTime TIMESTAMP
LocalDateTypeHandler java.time.LocalDate DATE
LocalTimeTypeHandler java.time.LocalTime TIME
OffsetDateTimeTypeHandler java.time.OffsetDateTime TIMESTAMP
OffsetTimeTypeHandler java.time.OffsetTime TIME
ZonedDateTimeTypeHandler java.time.ZonedDateTime TIMESTAMP
YearTypeHandler java.time.Year INTEGER
MonthTypeHandler java.time.Month INTEGER
YearMonthTypeHandler java.time.YearMonth VARCHARLONGVARCHAR
JapaneseDateTypeHandler java.time.chrono.JapaneseDate DATE

你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。比如:

// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
 ps.setString(i, parameter);
}

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
 return rs.getString(columnName);
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
 return rs.getString(columnIndex);
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
 return cs.getString(columnIndex);
}
}
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。

通过类型处理器的泛型,MyBatis 可以得知该类型处理器处理的 Java 类型,不过这种行为可以通过两种方法改变:

  • 在类型处理器的配置元素(typeHandler 元素)上增加一个 javaType 属性(比如:javaType="String");
  • 在类型处理器的类上增加一个 @MappedTypes 注解指定与其关联的 Java 类型列表。 如果在 javaType 属性中也同时指定,则注解上的配置将被忽略。

可以通过两种方式来指定关联的 JDBC 类型:

  • 在类型处理器的配置元素上增加一个 jdbcType 属性(比如:jdbcType="VARCHAR");
  • 在类型处理器的类上增加一个 @MappedJdbcTypes 注解指定与其关联的 JDBC 类型列表。 如果在 jdbcType 属性中也同时指定,则注解上的配置将被忽略。

当在 ResultMap 中决定使用哪种类型处理器时,此时 Java 类型是已知的(从结果类型中获得),但是 JDBC 类型是未知的。 因此 Mybatis 使用 javaType=[Java 类型], jdbcType=null 的组合来选择一个类型处理器。 这意味着使用 @MappedJdbcTypes 注解可以限制类型处理器的作用范围,并且可以确保,除非显式地设置,否则类型处理器在 ResultMap 中将不会生效。 如果希望能在 ResultMap 中隐式地使用类型处理器,那么设置 @MappedJdbcTypes 注解的 includeNullJdbcType=true 即可。 然而从 Mybatis 3.4.0 开始,如果某个 Java 类型只有一个注册的类型处理器,即使没有设置 includeNullJdbcType=true,那么这个类型处理器也会是 ResultMap 使用 Java 类型时的默认处理器。

最后,可以让 MyBatis 帮你查找类型处理器:

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

注意在使用自动发现功能的时候,只能通过注解方式来指定 JDBC 的类型。

你可以创建能够处理多个类的泛型类型处理器。为了使用泛型类型处理器, 需要增加一个接受该类的 class 作为参数的构造器,这样 MyBatis 会在构造一个类型处理器实例的时候传入一个具体的类。

//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {

  private Class<E> type;

  public GenericTypeHandler(Class<E> type) {
    if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
    this.type = type;
  }
  ...

EnumTypeHandlerEnumOrdinalTypeHandler 都是泛型类型处理器,我们将会在接下来的部分详细探讨。

处理枚举类型

若想映射枚举类型 Enum,则需要从 EnumTypeHandler 或者 EnumOrdinalTypeHandler 中选择一个来使用。

比如说我们想存储取近似值时用到的舍入模式。默认情况下,MyBatis 会利用 EnumTypeHandler 来把 Enum 值转换成对应的名字。

注意 EnumTypeHandler 在某种意义上来说是比较特别的,其它的处理器只针对某个特定的类,而它不同,它会处理任意继承了 Enum 的类。

不过,我们可能不想存储名字,相反我们的 DBA 会坚持使用整形值代码。那也一样简单:在配置文件中把 EnumOrdinalTypeHandler 加到 typeHandlers 中即可, 这样每个 RoundingMode 将通过他们的序数值来映射成对应的整形数值。

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>

但要是你想在一个地方将 Enum 映射成字符串,在另外一个地方映射成整形值呢?

自动映射器(auto-mapper)会自动地选用 EnumOrdinalTypeHandler 来处理枚举类型, 所以如果我们想用普通的 EnumTypeHandler,就必须要显式地为那些 SQL 语句设置要使用的类型处理器。

(下一节才开始介绍映射器文件,如果你是首次阅读该文档,你可能需要先跳过这里,过会再来看。)

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode"/>
	</resultMap>

	<select id="getUser" resultMap="usermap">
		select * from users
	</select>
	<insert id="insert">
	    insert into users (id, name, funkyNumber, roundingMode) values (
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode}
	    )
	</insert>

	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
	</resultMap>
	<select id="getUser2" resultMap="usermap2">
		select * from users2
	</select>
	<insert id="insert2">
	    insert into users2 (id, name, funkyNumber, roundingMode) values (
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=org.apache.ibatis.type.EnumTypeHandler}
	    )
	</insert>

</mapper>

注意,这里的 select 语句必须指定 resultMap 而不是 resultType

对象工厂(objectFactory)

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:

// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

ObjectFactory 接口很简单,它包含两个创建实例用的方法,一个是处理默认无参构造方法的,另外一个是处理带参数的构造方法的。 另外,setProperties 方法可以被用来配置 ObjectFactory,在初始化你的 ObjectFactory 实例后, objectFactory 元素体中定义的属性会被传递给 setProperties 方法。

插件(plugins)**

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

提示 覆盖配置类

除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。

8. 作用域(Scope)和生命周期

Mybatis流程:

image-20220429175952070

作用域(Scope)和生命周期

理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。

提示 对象生命周期和依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

映射器实例

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:

try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 你的应用逻辑代码
}

声明周期和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题。

SqlSessionFactoryBuilder:

  • 建造工程的工人,一旦创建了SqlSessionFactory,就不需要它了
  • 局部变量

SqlSessionFactory:

  • 可以想象为数据库连接池
  • SqlSessionFactory一旦被创建就应该在应用期间一直存在,没有任何理由丢弃它或重新创建另外一个实例
  • 因此SqlSessionFactory的最佳作用域就是应用作用域
  • 最简单的就是单例模式或静态单例模式,保证全局只有一份。

SqlSession:

  • 连接到连接池的一个请求
  • 需要开启和关闭
  • SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域
  • 用完之后需要赶紧关闭,否则资源被占用。

image-20220429180938254

每一个Mapper就代表一个具体的业务。


五、解决属性名和字段名不一致的问题

1. 问题引入

数据库中的字段:

image-20220429181108556

新建一个项目,拷贝之前的,测试实体类字段不一致的情况

旧实体类:

package com.dearliu.pojo;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private Integer id;
    private String name;
    private String pwd;

    private static final long serialVersionUID = 1L;
}

新实体类:

package com.laxsilence.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    
    private Integer id;
    private String name;
    private String password;

    private static final long serialVersionUID = 1L;
}

可以发现pwd改成了password

通过ID查询可以发现:

image-20220429182726282

查到的密码为空!这是为什么呢?

我们分析一下:

在mapper.xml中我们写的语句是这样的:select * from mybatis_study.user where id = #{id}

等价于(注意,数据库中的字段是pwd):select id,name,pwd from mybatis_study.user where id = #{id}

然后将查询到的三个值复制给User这个对象,再来看User中的参数:id,name,password ! 所以执行set方法的时候,只能赋值给name,id,而password却找不到对应的值!

前面我们提到过类型转换器:

image-20220429183052368

可以发现,如果数据库中字段命名为:A_Colume等等,只要中间有_,那么这个类型转换器就自动转为驼峰命名 即 aColume 比如说:user_password,在实体类中对应的应该是userPassword!

在本次测试中,因为数据库中的pwd无论如何也转换不成password,所以无法设置。

那么如何解决这个问题呢?

解决方法:

  • 起别名:

    将sql语句进行修改:select id , name , pwd as password from mybatis_study.user where id = #{id}

    image-20220429183619060

    但这个笨拙的解决方法还要求实体类中不能存在无参构造,所以你需要删掉无参构造,不然还是为空

  • 结果集映射

    我们对于该sql语句所在mapper.xml的编写如下:

    <select id="getUserById" parameterType="int" resultType="com.laxsilence.pojo.User">
        select * from mybatis_study.user where id = #{id}
    </select>
    

    此时我们将``resultType修改为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.laxsilence.dao.UserMapper">
    
        <!--结果集映射:将数据库中的字段与实体类中的属性一一对应,实现映射-->
        <resultMap id="UserMap" type="com.laxsilence.pojo.User">
            <!--column :数据库中的列,也就是字段-->
            <!--property: 实体类中的属性-->
            <result column="id" property="id"/>
            <result column="name" property="name"/>
            <result column="pwd" property="password"/>
    
        </resultMap>
        <select id="getUserById" parameterType="int" resultMap="UserMap">
            select * from mybatis_study.user where id = #{id}
        </select>
    
    </mapper>
    

    可以发现,属性名和字段名不一样的问题解决了,那又有一个新问题,如果字段映射的属性是某个类的对象呢?这个问题我们后面解决~

2. 结果映射

通过对官方文档进行总结:

  • resultMap元素是 MyBatis 中最重要最强大的元素。

  • ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

  • 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.laxsilence.dao.UserMapper">
    
        <resultMap id="UserMap" type="com.laxsilence.pojo.User">
            <!--什么需要映射,就映射什么-->
            <result column="pwd" property="password"/>
    
        </resultMap>
        <select id="getUserById" parameterType="int" resultMap="UserMap">
            select * from mybatis_study.user where id = #{id}
        </select>
    
    </mapper>
    
  • 但是官方文档还有一句话:

    如果世界总是这么简单就好了。

    后面还有一对多和多对多等着我们去学习。

    image-20220429185912916


六、日志

1. 日志工厂

如果一个数据库操作出现了异常,我们需要排错,所以日志就是最好的助手!

曾经的我们排错:sout、debug

现在:日志工程

在前文的配置解析中,设置一栏有很多的属性,我们可以找到这样一个:

image-20220429190443382

即:

  • SLF4J
  • LOG4J【掌握】
  • Log4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING【掌握】
  • NO_LOGGING

在Mybatis中具体使用哪一个日志实现,在设置中设定!

STDOUT_LOGGING标准日志输出:

在核心配置文件中的setting中配置:

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

运行测试类,控制台输出:

image-20220429191150750

2. LOG4J日志

什么是log4j?

  • Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;

  • 我们也可以控制每一条日志的输出格式

  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

  • 最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

我们按照使用STDOUT_LOGGING标准日志的方法,使用log4j:

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

发现报错,说找不到类,因此,需要再maven中导入依赖!

  1. 使用log4j,需要先导包

    <!--        log4j-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
    
  2. log4j.properties配置文件,放在resources目录下

    #将等级为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/laxsilence.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. 配置log4j为日志的实现

    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
    
  4. log4j的使用,直接运行测试类,发现差不多!

    image-20220501152856102

测试log4j的简单使用:

  1. 在要使用log4j的类中,导入包,记得导入log4j的包,不要导util包

  2. 生成日志对象,参数为当前类的class

    static Logger logger = Logger.getLogger(UserDaoTest.class);
    
  3. 测试类:

    @Test
    public void testLog4j(){
        //想象成输出就可以
        logger.info("info:进入了testLog4j");
        logger.debug("debug:进入了testLog4j");
        logger.error("error:进入了testLog4j");
    }
    
  4. 运行:

    image-20220501153924286

  5. 在配置文件中,我们可以设置很多东西,比如说,我们设置了将所有的日志信息生成一个logs的文件夹,我们可以打开这个文件夹看看!

    image-20220501154017092

    image-20220501154033644

    在这个文件中,我们可以看到一个日志的级别和生成的时间。

    INFO:提示信息

    DEBUG:排错

    ERROR:可能会出现错误

  6. 之后,我们可以去查看日志文件,通过头属性,可以很清楚知道这条日志是什么级别的。


七、分页

思考:为什么要分页?

  • 减少数据的处理量

1. 使用Limit分页,SQL实现分页:

语法:select * for user limit startIndex,pageSize;

补充:

  • 以前存在 limit 2,-1 这种语法,但是后来被当做bug修复
  • limit 3 :如果只有一个参数的话,意思是查询这个参数之前的所有值,比如limit 5 ,就会显示前5条数据,左闭右开,从0开始计数,显示0,1,2,3,4,这5条数据

使用Mybatis实现分页,核心还是SQL

  1. 接口

    //分页测试
    List<User> getUserByLimit(Map<String,Object> map);
    
  2. Mapper.xml

    <!--注意结果集映射-->
    <select id="getUserByLimit" parameterType="map"  resultMap="UserMap">
        select * from mybatis_study.user limit #{startIndex},#{pageSize};
    </select>
    
  3. 测试

    @Test
    public void getByLimit(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        Map<String,Object> map = new HashMap<>();
        map.put("startIndex",1);
        map.put("pageSize",2);
        List<User> userByLimit = mapper.getUserByLimit(map);
        for (User user : userByLimit) {
            System.out.println(user.toString());
        }
        sqlSession.close();
    }
    

我们发现使用sql分页确实可以,但如何体现出Java的面向对象呢?

于是一群人弄出了RowBounds分页,但是并不推荐使用,因为它不如sql分页好使。

2. RowBounds实现分页(逻辑,通过Java代码实现分页):

  1. 接口:

    //分页测试2
    List<User> getUserByRowBounds();
    
  2. Mapper.xml

    <select id="getUserByRowBounds" parameterType="map"  resultMap="UserMap">
        select * from mybatis_study.user;
    </select>
    
  3. 测试

    @Test
    public void testRowBounds(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        //RowBounds 分页
        RowBounds rowBounds = new RowBounds(0,2);
        List<User> userList = sqlSession.selectList("com.laxsilence.dao.UserMapper.getUserByRowBounds",null,rowBounds);
        for (User user : userList) {
            System.out.println(user);
        }
    
        sqlSession.close();
    }
    

3. 分页插件

image-20220501162342421

了解即可,使用的时候很简单:

官网:https://pagehelper.github.io

官方使用文档:https://pagehelper.github.io/docs/howtouse/


八、使用注解开发

1. 面向接口编程

大家之前都学过面向对象编程,也学习过接口,但在真正的开发中,很多时候我们会选择面向接口编程

根本原因:解耦,可拓展,提高复用,分层开发中,上层不用管具体的实现,大家都遵守共同的标准,使得开发变得容易,规范性更好

  • 在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下,各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了;

  • 而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信,大到各模块之间的交互,在系统设计之初都是要着重考虑的,这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。

关于接口的理解

  • 接口从更深层次的理解,应是定义(规范,约束)与实现(名实分离的原则)的分离。

  • 接口的本身反映了系统设计人员对系统的抽象理解。

  • 接口应有两类:

    • 第一类是对一个个体的抽象,它可对应为一个抽象体(abstract class);
    • 第二类是对一个个体某一方面的抽象,即形成一个抽象面(interface);
  • 一个体有可能有多个抽象面。抽象体与抽象面是有区别的。

三个面向区别:

  • 面向对象是指,我们考虑问题时,以对象为单位,考虑它的属性及方法
  • 面向过程是指,我们考虑问题时,以一个具体的流程(事务过程)为单位,考虑它的实现
  • 接口设计与非接口设计是针对复用技术而言的,与面向对象(过程)不是一个问题,更多的体现就是对系统整体的架构

2. 使用注解开发

<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>

对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}

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

(1)测试注解开发

使用注解开发:

  1. 不需要mapper.xml文件,不需要再核心配置文件中绑定mapper

  2. 在接口的方法上添加注解

    //查询用户
    @Select("SELECT * FROM mybatis_study.user")
    List<User> getUsers();
    
  3. 在核心配置文件中绑定接口类

    <!-- 之前是绑定配置文件,现在绑定接口-->
    <mappers>
        <mapper class="com.laxsilence.dao.UserMapper"></mapper>
    </mappers>
    
  4. 测试:

    @Test
    public void test(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = mapper.getUsers();
        for (User user : users) {
            System.out.println(user.toString());
        }
    }
    
  5. 查询结果:

    image-20220501165527228

我们可以发现:确实能够查出数据,但是密码都为null,这是因为字段和属性不匹配的问题,所以官方文档才说,如果是过复杂的问题,建议使用xml配置文件解决!!!

(2)注解开发的原理

通过debug,我们进行深层次的探讨,可以发现在测试类,通过Factory生成SqlSession的一个对象,这个对象中的东西其实就是核心配置文件中配置的内容!

image-20220501165318607

sqlSession对象,通过sqlSession.getMapper(UserMapper.class),通过反射找到了包下的这个类,获得了这个类的一个实例化对象,即 UserMapper mapper;

image-20220501170136115

总结:

  1. 本质:反射机制实现
  2. 底层:动态代理

推荐阅读注解的实现过程: https://www.zhihu.com/question/24401191/answer/1724982163

3. Mybatis详细的执行流程

Resources获取加载全局配置文件 ---> 实例化SqlSessionFactoryBuilder构造器 ---> 解析配置文件流XMLConfigBuilder ---> Configuration中有所有的配置信息 ---> SqlSessionFactory实例化 ---> transactional事务管理 ---> 创建executor执行器 ---> 创建sqlSession ---> 实现CRUD ---> 查看是否执行成功(失败返回transactional事务管理)---> 提交事务 ---> 关闭!

可以debug自己多跑一跑~

4. 注解实现CRUD

我们可以在工具类创建的时候实现自动提交事务,在MyBatisUtils工具类中将sqlSessionFactory.openSession(true);参数设为true,开启自动提交,默认不写为false,这样后面的增删改就不需要手动提交了。

但是最好不要开启自动提交!!!

package com.laxsilence.util;

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 java.io.IOException;
import java.io.InputStream;

//工厂设计模式
//sqlSessionFactory  -- > 用来生产sqlSession (类似于jdbc中的prepareStatement,用来执行sql语句)
public class MyBatisUtils {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            // 使用MyBatis第一步:  获取 sqlSessionFactory 对象
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //    既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
//    SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句
    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession(true);
    }
}

通过ID查询

  1. 接口:

    //通过ID查询,方法存在多个参数,所有的参数前面必须加上@Param注解,执行查询的时候,以注解中@Param中的属性名为主,形参名无关
    @Select("SELECT * FROM mybatis_study.user WHERE id=#{id}")
    User getUserByID(@Param("id")int id);
    
  2. 测试:

    @Test
    public void test1(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User userByID = mapper.getUserByID(1);
        System.out.println(userByID);
        sqlSession.close();
    }
    

增加用户:

  1. 接口:

    //增加用户
    @Insert("insert into mybatis_study.user(id, name, pwd) VALUES (#{id},#{name},#{password})")
    int addUser(User user);
    
  2. 测试:

    @Test
    public void test2(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.addUser(new User(6,"nihao","1565465"));
        sqlSession.close();
    }
    

    因为我们设置了自动提交,所以测试中不需要手动提交!

修改用户数据

  1. 接口:

    //修改用户
    @Update("UPDATE mybatis_study.user set name=#{name},pwd=#{password} where id=#{id} ")
    int updateUser(User user);
    
  2. 测试

    @Test
    public void test3(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.updateUser(new User(6,"nihaoya","123121"));
        sqlSession.close();
    }
    

删除用户数据

  1. 接口

    //删除用户
    @Delete("delete from mybatis_study.user where id=#{uid}")
    int deleteUser(@Param("uid")int id);
    
  2. 测试

    @Test
    public void test4(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
    
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.deleteUser(5);
        sqlSession.close();
    }
    

5. 关于@Param() 注解

  • 基本类型的参数或者String类型的参数都需要加上
  • 引用类型的参数不需要加
  • 如果只有一个基本类型的话可以忽略,但还是建议加上~
  • 我们在SQL中引用的就是这里的@Param()中设定的属性名!

九、Lombok的使用

是把双刃剑,便捷但又危害。一个人使用了,全团队都得使用才能正常运行。

官网介绍:

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.

Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

翻译:

Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。

永远不要再编写另一个 getter 或 equals 方法,使用一个注释,您的类就有一个功能齐全的构建器、自动化您的日志记录变量等等。

Lombok的使用:

  1. 安装Lombok插件,IDEA版本在2020之后已经自带

    拥有的注释:

    @Getter and @Setter
    @FieldNameConstants
    @ToString
    @EqualsAndHashCode
    @AllArgsConstructor, @RequiredArgsConstructor and @NoArgsConstructor
    @Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j, @CommonsLog, @JBossLog, @Flogger, @CustomLog
    @Data
    @Builder
    @SuperBuilder
    @Singular
    @Delegate
    @Value
    @Accessors
    @Wither
    @With
    @SneakyThrows
    @val
    @var
    experimental @var
    @UtilityClass
    
  2. 导入依赖库

    <!--        lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.16</version>
            </dependency>
    
  3. 实体类使用

    在没有加注解前:

    image-20220501185211434

    添加注解@Data:

    package com.laxsilence.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.io.Serializable;
    
    @Data
    public class User implements Serializable {
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        private static final long serialVersionUID = 1L;
    }
    

    瞬间多出来很多方法:

    image-20220501185248507

    再来添加有参构造:

    @AllArgsConstructor,但是我们会发现,添加有参构造后,@Data生成的无参构造消失了,所以需要添加无参构造

    @NoArgsConstructor:无参构造

目前不需要理解其原理,较为复杂~


十、多对一

多对一:

image-20220501190706773

  • 多个学生,对应一个老师

  • 对于学生这边而言,【多对一】-- 关联 -- 多个学生关联一个老师

    association – 一个复杂类型的关联;许多结果将包装成这种类型

  • 对于老师而言,【一对多】-- 集合 -- 一个老师有多个学生

    collection – 一个复杂类型的集合

结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中
    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一个复杂类型的集合
    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个 resultMap
    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
type 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。

最佳实践 最好逐步建立结果映射。单元测试可以在这个过程中起到很大帮助。 如果你尝试一次性创建像上面示例那么巨大的结果映射,不仅容易出错,难度也会直线上升。 所以,从最简单的形态开始,逐步迭代。而且别忘了单元测试! 有时候,框架的行为像是一个黑盒子(无论是否开源)。因此,为了确保实现的行为与你的期望相一致,最好编写单元测试。 并且单元测试在提交 bug 时也能起到很大的作用。

下一部分将详细说明每个元素。

1. 测试环境搭建

准备工作:

  1. 创建数据库

    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. 新建实体类:

    image-20220501193725433

    package com.laxsilence.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Student {
    
        private Integer id;
    
        private String name;
    
        //学生需要关联一个老师
        private Teacher teacher;
    }
    
    package com.laxsilence.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Teacher {
    
        private Integer id;
    
        private String name;
    }
    
  3. 新建Mapper接口

    image-20220501193757721

    package com.laxsilence.dao;
    
    public interface StudentMapper {
    }
    
    package com.laxsilence.dao;
    
    public interface TeacherMapper {
    }
    
  4. 新建mapper映射文件

    注意:我们发现dao里内容比较多,所以将映射文件建立在resources包中,切记同包名,在resources文件夹新建包的时候要这样写:com/laxsilence/dao,不能使用.要用/,这样的话,最后生成的文件中,xml所在的包和接口所在的包会合并在一起!

    image-20220501195736614

    在新建映射文件的时候,我们可以把mybatis-config的内容复制过来,然后将所有的configuration改成mapper

    image-20220501194314558

    image-20220501194351357

    然后在开始加 namespace,选中要绑定的接口,比如TeacherMapper.xml 就选择com.laxsilence.dao.TeacherMapper

  5. 在核心配置文件中进行绑定,我们会使用注解和mapper.xml,所以可以都绑定了,还可以起别名:

    <?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 核心配置文件-->
    <configuration>
        <properties resource="db.properties"></properties>
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
        <typeAliases>
            <typeAlias type="com.laxsilence.pojo.Teacher" alias="Teacher"/>
            <typeAlias type="com.laxsilence.pojo.Student" alias="Student"/>
        </typeAliases   >
        <!--设置默认环境-->
        <environments default="development">
            <!--可以有多个环境-->
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}"/>
                    <property name="url" value="${url}"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}"/>
                </dataSource>
            </environment>
        </environments>
        <!-- 之前是绑定配置文件,现在绑定接口-->
        <!--当使用注解的时候,这个绑定没用-->
        <mappers>
            <mapper class="com.laxsilence.dao.TeacherMapper"></mapper>
            <mapper class="com.laxsilence.dao.StudentMapper"></mapper>
            <mapper resource="com/laxsilence/dao/TeacherMapper.xml"></mapper>
            <mapper resource="com/laxsilence/dao/StudentMapper.xml"></mapper>
        </mappers>
    </configuration>
    
  6. 测试类:

    package com.laxsilence;
    
    public class MyTest {
        }
    }
    

开始测试项目:

  1. Teacher接口:

    @Select("select * from mybatis_study.teacher where id=#{tid} ")
    Teacher getTeacher(@Param("tid") int id);
    
  2. 测试:

    package com.laxsilence;
    
    import com.laxsilence.dao.TeacherMapper;
    import com.laxsilence.pojo.Teacher;
    import com.laxsilence.utils.MyBatisUtils;
    import org.apache.ibatis.session.SqlSession;
    import org.junit.Test;
    
    public class MyTest {
        @Test
        public void teacherTest(){
            SqlSession sqlSession = MyBatisUtils.getSqlSession();
            TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
            Teacher teacher = mapper.getTeacher(1);
            System.out.println(teacher);
            sqlSession.close();
        }
    }
    
  3. 运行成功:

    image-20220501200018584

2. 多对一的处理

回顾MySQL多对一的查询方式:

  • 子查询
  • 联表查询

子查询对应的就是按照查询嵌套处理

联表查询对应的就是按照结果嵌套处理

(1)按照查询嵌套处理

接口:

package com.laxsilence.dao;

import com.laxsilence.pojo.Student;

import java.util.List;

public interface StudentMapper {
    //查询所有的学生信息,以及对应的老师的信息
    public List<Student> getStudent();
}

mapper.xml:

<?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.laxsilence.dao.StudentMapper">
       <!--方式1:(子查询 按照查询嵌套处理)
        1. 查询所有的学生信息
        2.根据查询出来的学生的tid,寻找对应的老师
    -->
    <select id="getStudent" resultMap="StudentTeacher">
        select * from mybatis_study.student;
    </select>
    <!--如何将这两个查询语句关联起来-->
    <resultMap id="StudentTeacher" type="Student">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!--复杂的属性(对象或集合),我们需要单独处理 对象:association  集合:collection -->
        <!--由于teacher属性是一个Java的Teacher对象,而不是一个简单的属性,所以需要声明javaType,并且用select查出这个对象的信息-->
        <association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
    </resultMap>
    <select id="getTeacher" resultType="Teacher">
        select * from mybatis_study.teacher where id = #{tid} <!--注意这里的#{}里可以随意,它会自动匹配成column="tid"-->
    </select>
</mapper>

测试类:

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

结果:

image-20220506155310979

(2)按照结果嵌套处理

接口:

public List<Student> getStudent2();

mapper.xml:

<!--方式2:(按照结果嵌套处理)-->
<select id="getStudent2" resultMap="StudentTeacher2">
    select s.id sid,s.name sname,t.name tname
    from mybatis_study.student s,mybatis_study.teacher t
    where s.tid = t.id;
</select>
<resultMap id="StudentTeacher2" type="Student">
    <!--起别名之后,对应的数据库中的列要用别名-->
    <result property="id" column="sid"/>
    <result property="name" column="sname"/>
    <association property="teacher" javaType="Teacher">
        <result property="name" column="tname"/>
    <!--  <result property="id" column="id"/>    可以省略,字段和属性相同-->
    </association>
</resultMap>

测试:

public void testGetStudent2(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<Student> student = mapper.getStudent2();
    for (Student student1 : student) {
        System.out.println(student1);
    }
    sqlSession.close();
}

结果:因为只查询了老师的姓名,在sql里面没有查询老师的id,所以id为null

image-20220506155207423


十一、一对多

一个老师拥有多个学生,对于老师而言就是一对多的关系

1. 测试环境搭建

过程和刚才一样

实体类的变化:

学生类:学生成为了一个单独的个体,不需要关联老师

package com.laxsilence.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {

    private Integer id;

    private String name;

    private int tid;
}

老师类:老师是一对多,需要包含每一个学生

package com.laxsilence.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {

    private Integer id;

    private String name;

    //一个老师拥有多个学生
    private List<Student> students;
}

2. 一对多的处理

(1)按照查询嵌套处理

接口:

//获取指定老师下的所有学生及老师的信息
Teacher getTeacher2(@Param("tid") int id);

mapper.xml:

<!--方式1:子查询-->
<select id="getTeacher2" resultMap="TeacherStudent2" >
    select * from mybatis_study.teacher where id =#{tid};
</select>
<resultMap id="TeacherStudent2" type="Teacher" >
    <collection property="students" column="id" javaType="ArrayList" ofType="Student" select="getStudentByTeacherId"/>
</resultMap>
<select id="getStudentByTeacherId" resultType="Student">
    select * from mybatis_study.student where tid = #{tid}
</select>

测试:

SqlSession sqlSession = MyBatisUtils.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacher(1);
System.out.println(teacher);
/* 查询结果:
 Teacher(
 id=1,
 name=秦老师,
 students=
 [Student(id=1, name=小明, tid=1),
 Student(id=2, name=小红, tid=1),
 Student(id=3, name=小张, tid=1),
 Student(id=4, name=小李, tid=1),
 Student(id=5, name=小王, tid=1)]
 )
**/
sqlSession.close();

(2)按照结果嵌套处理

接口:

Teacher getTeacher(@Param("tid") int id);

mapper.xml

<!--方式2:按结果嵌套查询-->
<select id="getTeacher" resultMap="TeacherStudentMap">
    select s.id sid,s.name sname,t.name tname,t.id tid
    from mybatis_study.student s,mybatis_study.teacher t
    where s.tid = t.id
    and t.id = #{tid}
</select>
<resultMap id="TeacherStudentMap" type="Teacher">
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <!--复杂的属性,我们需要单独处理 对象使用association 集合使用collection-->
    <!--
        javaType=“”:pojo类中属性的类型
        而集合中的泛型,使用ofType获取
    -->
    <collection property="students" ofType="Student">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

测试:

@Test
public void testGetTeacher2(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
    Teacher teacher = mapper.getTeacher2(1);
    System.out.println(teacher);
    sqlSession.close();
}

3. 小结多对一和一对多

多个学生,对应一个老师

  • 对于学生这边而言,【多对一】-- 关联 -- 多个学生关联一个老师

    association – 一个复杂类型的关联;许多结果将包装成这种类型

  • 对于老师而言,【一对多】-- 集合 -- 一个老师有多个学生

    collection – 一个复杂类型的集合

  • javaType:用来指定实体类中属性的类型

  • ofType:用来指定映射到List或集合中泛型的约束的类型

注意点:

  1. 保证SQL的可读性,尽量保证通俗易懂,方便维护
  2. 注意一对多和多对一中属性名和字段的问题
  3. 如果问题不好排查,可以使用日志

这时候我们再回过头来看第五章中结果映射的内容:

image-20220506170213328

结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中
    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一个复杂类型的集合
    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个 resultMap
    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
type 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。

最佳实践 最好逐步建立结果映射。单元测试可以在这个过程中起到很大帮助。 如果你尝试一次性创建像上面示例那么巨大的结果映射,不仅容易出错,难度也会直线上升。 所以,从最简单的形态开始,逐步迭代。而且别忘了单元测试! 有时候,框架的行为像是一个黑盒子(无论是否开源)。因此,为了确保实现的行为与你的期望相一致,最好编写单元测试。 并且单元测试在提交 bug 时也能起到很大的作用。

下一部分将详细说明每个元素。

补充:面试高频

  • MySQL引擎
  • InnoDB的底层原理
  • 索引
  • 索引优化

十二、动态SQL

什么是动态SQL:

  • 动态SQL就是指根据不同的条件生成不同的SQL语句。

动态SQL

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

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

script

要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:

    @Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
    void updateAuthorValues(Author author);

bind

bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

多数据库支持

如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

动态 SQL 中的插入脚本语言**

MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。

可以通过实现以下接口来插入一种语言:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

或者,你也可以使用 lang 属性为特定的语句指定语言:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

或者,在你的 mapper 接口上添加 @Lang 注解:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

提示 可以使用 Apache Velocity 作为动态语言,更多细节请参考 MyBatis-Velocity 项目。

你前面看到的所有 xml 标签都由默认 MyBatis 语言提供,而它由语言驱动 org.apache.ibatis.scripting.xmltags.XmlLanguageDriver(别名为 xml)所提供。

1. 搭建环境

建立数据库

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

创建一个基础工程:

  1. 导包

  2. 编写配置文件

    <?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 核心配置文件-->
    <configuration>
        <properties resource="db.properties"></properties>
        <settings>
            <!--日志实现-->
            <setting name="logImpl" value="STDOUT_LOGGING"/>
            <!--开启驼峰命名-->
            <setting name="mapUnderscoreToCamelCase" value="true"/>
        </settings>
        <typeAliases>
            <typeAlias type="com.laxsilence.pojo.Blog" alias="Blog"/>
        </typeAliases   >
        <!--设置默认环境-->
        <environments default="development">
            <!--可以有多个环境-->
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}"/>
                    <property name="url" value="${url}"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}"/>
                </dataSource>
            </environment>
        </environments>
        <!-- 之前是绑定配置文件,现在绑定接口-->
        <!--当使用注解的时候,这个绑定没用-->
        <mappers>
            <mapper class="com.laxsilence.dao.BlogMapper"></mapper>
            <mapper resource="com/laxsilence/dao/BlogMapper.xml"></mapper>
        </mappers>
    </configuration>
    
  3. 编写实体类和工具类

    实体类:

    package com.laxsilence.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.Date;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Blog {
        private String id;
        private String title;
        private String author;
        private Date createTime;
        private int views;
    }
    

    UUID类:

    package com.laxsilence.utils;
    
    import org.junit.Test;
    
    import java.util.UUID;
    
    public class IDUtils {
        public static String getID(){
            return UUID.randomUUID().toString().replaceAll("-","");
        }
        @Test
        public void testUUID(){
            System.out.println(IDUtils.getID());
        }
    }
    

    工厂类:

    package com.laxsilence.utils;
    
    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 java.io.IOException;
    import java.io.InputStream;
    
    //工厂设计模式
    //sqlSessionFactory  -- > 用来生产sqlSession (类似于jdbc中的prepareStatement,用来执行sql语句)
    public class MyBatisUtils {
    
        private static SqlSessionFactory sqlSessionFactory;
    
        static {
            try {
                // 使用MyBatis第一步:  获取 sqlSessionFactory 对象
                String resource = "mybatis-config.xml";
                InputStream inputStream = Resources.getResourceAsStream(resource);
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //    既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
    //    SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句
        public static SqlSession getSqlSession(){
            return sqlSessionFactory.openSession(true);
        }
    }
    
  4. 编写实体类对应的mapper和mapper.xml文件

    package com.laxsilence.dao;
    
    public interface BlogMapper {
    }
    
    <?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.laxsilence.dao.BlogMapper">
    
    
    </mapper>
    
  5. 数据库中插入数据

    • 接口:

      //插入数据
      int addBlog(Blog blog);
      
    • mapper.xml

      <insert id="addBlog" parameterType="blog">
          insert into mybatis_study.blog(id, title, author, create_time, views)
          VALUES (#{id},#{title},#{author},#{createTime},#{views});
      </insert>
      
    • 测试

      import com.laxsilence.dao.BlogMapper;
      import com.laxsilence.pojo.Blog;
      import com.laxsilence.utils.IDUtils;
      import com.laxsilence.utils.MyBatisUtils;
      import org.apache.ibatis.session.SqlSession;
      import org.junit.Test;
      
      import java.util.Date;
      
      public class MyTest {
          @Test
          public void addBlogTest() {
              SqlSession sqlSession = MyBatisUtils.getSqlSession();
              BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
              Blog blog = new Blog();
              blog.setId(IDUtils.getID());
              blog.setTitle("Mybatis如此简单");
              blog.setAuthor("狂神说");
              blog.setCreateTime(new Date());
              blog.setViews(9999);
      
              mapper.addBlog(blog);
      
              blog.setId(IDUtils.getID());
              blog.setTitle("Java如此简单");
              mapper.addBlog(blog);
      
              blog.setId(IDUtils.getID());
              blog.setTitle("Spring如此简单");
              mapper.addBlog(blog);
      
              blog.setId(IDUtils.getID());
              blog.setTitle("微服务如此简单");
              mapper.addBlog(blog);
      
              sqlSession.close();
          }
      }
      
      
  6. 查看数据库结果

    image-20220506182724084

2. 动态SQL元素:从IF入门

if

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

<select id="findActiveBlogWithTitleLike"
   resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
  AND title like #{title}
</if>
</select>

这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码或通配符字符)。

如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。

<select id="findActiveBlogLike"
   resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
  AND title like #{title}
</if>
<if test="author != null and author.name != null">
  AND author_name like #{author.name}
</if>
</select>

接口:

//查询博客<if>
List<Blog> querryBlogIF(Map map);

mapper.xml:

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

测试:

@Test
    public void testQuerryBlog(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        HashMap map = new HashMap();
//        map.put("title","Java如此简单");
        map.put("author","狂神说");
        List<Blog> blogs = mapper.querryBlogIF(map);
        for (Blog blog : blogs) {
            System.out.println(blog);
        }
        sqlSession.close();
    }

3. 动态SQL元素:常用标签

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员挑选的 Blog)。

<select id="findActiveBlogLike"
  resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
 <when test="title != null">
   AND title like #{title}
 </when>
 <when test="author != null and author.name != null">
   AND author_name like #{author.name}
 </when>
 <otherwise>
   AND featured = 1
 </otherwise>
</choose>
</select>

trim、where、set

前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。

<select id="findActiveBlogLike"
  resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
 state = #{state}
</if>
<if test="title != null">
 AND title like #{title}
</if>
<if test="author != null and author.name != null">
 AND author_name like #{author.name}
</if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike"
  resultType="Blog">
SELECT * FROM BLOG
<where>
 <if test="state != null">
      state = #{state}
 </if>
 <if test="title != null">
     AND title like #{title}
 </if>
 <if test="author != null and author.name != null">
     AND author_name like #{author.name}
 </if>
</where>
</select>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

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

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
update Author
 <set>
   <if test="username != null">username=#{username},</if>
   <if test="password != null">password=#{password},</if>
   <if test="email != null">email=#{email},</if>
   <if test="bio != null">bio=#{bio}</if>
 </set>
where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

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

注意,我们覆盖了后缀值设置,并且自定义了前缀值。

(1)where 标签

在if中我们使用了where 1=1 来防止后面条件满足后出现 where and ... 的情况,但这样是不规范的,所以我们需要where标签

对if中的mapper.xml进行修改:去掉where 1 =1,改用where标签

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

(2)set标签

set的作用跟where类似,可以自动添加set进入语句,并自动去掉多余的逗号

接口:

//更新博客
int updateBlog(Map map);

mapper.xml:

<update id="updateBlog" parameterType="map">
    update mybatis_study.blog
        <set>
            <if test="title != null">
                title = #{title},
            </if>
            <if test="author != null">
                author = #{author}
            </if>
        </set>
    where id = #{id}
</update>

测试:

@Test
public void testUpdateBlog(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    HashMap map = new HashMap();
    map.put("title","Java如此简单2");
    map.put("author","狂神说");
    map.put("id","5f4f6cb5986e46e08ccb5e0f8f0ddec3");
    mapper.updateBlog(map);
    sqlSession.close();
}

(3)trim标签

自定义标签

where标签的原理:

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

set标签的原理:

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

(4)choose、when、otherwise标签

接口:

//查询博客<choose>
List<Blog> querryBlogChoose(Map map);

mapper.xml:

<select id="querryBlogChoose" parameterType="map" resultType="blog">
    select * from mybatis_study.blog
    <where>
       <choose>
           <when test="title != null">
               title = #{title}
           </when>
           <when test="author != null">
               and author = #{author}
           </when>
           <!--这个条件至少要满足,类似于switch case 中的default-->
           <otherwise>
               and views = #{views}
           </otherwise>
       </choose>
    </where>
</select>

实现:

@Test
public void testQuerryBlogChoose(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    HashMap map = new HashMap();
    map.put("title","Java如此简单");
    map.put("author","狂神说");
    map.put("views",9999);
    List<Blog> blogs = mapper.querryBlogChoose(map);
    for (Blog blog : blogs) {
        System.out.println(blog);
    }
    /*可以发现,只要满足select语句中的第一个条件,其他条件就不在执行,相当于每一个条件中都有break*/
    sqlSession.close();
}

到这里我们可以发现,所谓的动态SQL,本质还是sql语句,只是我们可以在SQL层面,执行一个逻辑代码。

4. 动态SQL元素:SQL片段

有的时候,我们可能会将一些功能部分抽取出来,方便复用!

  1. 使用SQL标签抽取出来要复用的内容:

    <sql id="if-title-author">
        <if test="title != null">
            title = #{title},
        </if>
        <if test="author != null">
            author = #{author}
        </if>
    </sql>
    
  2. 在需要的地方使用include导入

    <select id="querryBlogIF" parameterType="map" resultType="blog">
        select * from mybatis_study.blog
        <where>
            <include refid="if-title-author"></include>
        </where>
    </select>
    

注意事项:

  • 最好基于单表来定义SQL片段,不要定义太复杂的内容,因为复杂以为着重用概率低
  • 不要存在where和set标签等影响复用的东西

5. 动态SQL元素:ForEach

foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

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

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。

例如:

seclet * from user where 1=1 and (id=1 or id=2 or id=3)  就可以写成下面这种格式:

seclet * from user where 1=1
<foreach item="id" collection="ids" open="(" separator="or" close=")">
    #{id}
</foreach>

博客实战:

接口(为了方便测试,我们先将表的id修改为1,2,3,4):

//查询123号记录的博客
List<Blog> queryBlogForeach();

mapper.xml

<!--传递一个map,map中有集合-->
<select id="queryBlogForeach" parameterType="map" resultType="blog">
    select * from mybatis_study.blog
    <where>
        <foreach collection="ids" item="id" open="and (" separator=" or "  close=")">
            id = #{id}
        </foreach>
    </where>
</select>

测试:

@Test
public void testSelectBlog(){
    SqlSession sqlSession = MyBatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    HashMap map = new HashMap();
    ArrayList<Integer> ids = new ArrayList<Integer>();
    ids.add(1);
    ids.add(2);
    ids.add(3);
    map.put("ids",ids);
    List<Blog> blogs = mapper.queryBlogForeach(map);
    for (Blog blog : blogs) {
        System.out.println(blog);
    }
    sqlSession.close();
}

动态SQL就讲到这里,其实动态SQL就是在拼接SQL语句,我们只要保证SQL的正确性,按照SQL的格式,去排列组合就可以了

建议:现在MySQL的控制台里写出完整的SQL语句,再对应的去修改成为动态SQL,可以抽取实现复用。


十三、缓存

1. 简介

查询:连接数据库,耗费资源。

如何解决?将一次查询的结果,给他暂存到一个可以直接取到的地方!---> 内存,即缓存

这个时候,我们再次查询相同数据库的时候,直接走缓存,就不用走数据库了

  1. 什么是缓存【catch】?
    • 存在内存中的临时数据
    • 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。(三高:高并发,高可用,高性能)
  2. 为什么使用缓存?
    • 减少和数据库的交互次数,减少系统开销,提高系统效率。
  3. 什么样的数据能使用缓存?
    • 经常查询并且不经常改变的数据。

2. MyBatis缓存

MyBatis包含了一个非常强大的查询缓存特性,它可以非常方便的定制和配置缓存。缓存可以极大的提升查询效率。

MyBatis系统中默认定义了两级缓存,一级缓存二级缓存

  • 默认情况下:只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)

  • 二级缓存需要手动开启和配置,也是基于namespace级别的缓存

  • 为了提高拓展性,MyBatis定义了缓存接口Catch。我们可以通过实现catch接口来自定义二级缓存。

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //
    
    package org.apache.ibatis.cache;
    
    import java.util.concurrent.locks.ReadWriteLock;
    
    public interface Cache {
        String getId();
    
        void putObject(Object var1, Object var2);
    
        Object getObject(Object var1);
    
        Object removeObject(Object var1);
    
        void clear();
    
        int getSize();
    
        default ReadWriteLock getReadWriteLock() {
            return null;
        }
    }
    
  • 缓存

    MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

    默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

    <cache/>
    

    基本上就是这样。这个简单语句的效果如下:

    • 映射语句文件中的所有 select 语句的结果将会被缓存。
    • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
    • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
    • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
    • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
    • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

    提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

    这些属性可以通过 cache 元素的属性来修改。比如:

    <cache
      eviction="FIFO"
      flushInterval="60000"
      size="512"
      readOnly="true"/>
    

    这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

    可用的清除策略有:

    • LRU – 最近最少使用:移除最长时间不被使用的对象。
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
    • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

    默认的清除策略是 LRU。

    flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

    size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

    readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

    提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

    使用自定义缓存

    除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

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

    这个示例展示了如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。

    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();
    }
    

    为了对你的缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如,下面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法:

    <cache type="com.domain.something.MyCustomCache">
      <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
    </cache>
    

    你可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。 你也可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值。

    从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。 如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

    public interface InitializingObject {
      void initialize() throws Exception;
    }
    

    提示 上一节中对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。

    请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:

    <select ... flushCache="false" useCache="true"/>
    <insert ... flushCache="true"/>
    <update ... flushCache="true"/>
    <delete ... flushCache="true"/>
    

    鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

    cache-ref

    回想一下上一节的内容,对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。

    <cache-ref namespace="com.someone.application.data.SomeMapper"/>
    

3. 一级缓存

一级缓存也叫作本地缓存,SqlSession

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

测试步骤:

  1. 在核心配置文件中开启日志

  2. 接口:

    public interface UserMapper {
        User queryUserById(@Param("id") int id);
    }
    
  3. mapper.xml

    <?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.laxsilence.dao.UserMapper">
        <select id="queryUserById" resultType="user">
            select * from mybatis_study.user where id=#{id}
        </select>
    </mapper>
    
  4. 测试:

    public class MyTest {
        @Test
        public void test(){
            SqlSession sqlSession = MyBatisUtils.getSqlSession();
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user = mapper.queryUserById(1);
            System.out.println(user);
            System.out.println("==================================");
            User user2 = mapper.queryUserById(1);
            System.out.println(user2);
            System.out.println(user==user2);
            sqlSession.close();
        }
    }
    

    image-20220507191720468

缓存失效的问题:

  1. 查询不同的东西

  2. 增删改操作可能会改变原来的数据,所以必定会刷新缓存

    @Test
    public void test(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.queryUserById(1);
        System.out.println(user);
        mapper.updateUser(new User(2, "aaa", "000"));
        System.out.println("==================================");
        User user2 = mapper.queryUserById(1);
        System.out.println(user2);
        System.out.println(user==user2);
        sqlSession.close();
    }
    
  3. 查询不同的Mapper.xml

  4. 手动清理缓存!

    sqlSession.clearCache();//手动清理缓存
    

小结:

  • 一级缓存默认是开启的,只在一次SqlSession中有用,也就是拿到连接和关闭连接这个区间段。
  • 一级缓存就是一个Map集合,对它进行存取,用完就删掉。

4. 二级缓存

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

基于namespace级别的缓存,一个命名空间对应一个二级缓存。

不同的Mapper,但是只要是一个命名空间,那么就是同一个二级缓存!!!

工作机制:

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

步骤:

  1. 开启全局缓存

    配置文件中的设置里有一个cacheEnable:

    image-20220507193356341

    虽然它是默认开启的,但是我们一般都会显示的写出来

    <setting name="cacheEnable" value="true"/>
    
  2. 在要使用二级缓存的mapper中开启,即mapper.xml中进行配置

    <!--使用二级缓存-->
    <!--<cache/>可以只写这么一点点,也可以像下面一样自定义参数-->
    <!--readonly为true时,	读相同实例,false时读拷贝值,相对安全-->
    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
    
    <!--可以给某一条语句显示的说明使用或不使用cache-->
    <select id="queryUserById" resultType="user" useCache="true">
        select * from mybatis_study.user where id=#{id}
    </select>
    
  3. 测试

    @Test
    public void test2(){
        SqlSession sqlSession = MyBatisUtils.getSqlSession();
        SqlSession sqlSession2 = MyBatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        UserMapper mapper1 = sqlSession2.getMapper(UserMapper.class);
        User user = mapper.queryUserById(1);
        System.out.println(user);
        sqlSession.close();
        System.out.println("==================================");
        User user2 = mapper1.queryUserById(1);
        System.out.println(user2);
        System.out.println(user==user2);
        sqlSession2.close();
    
    }
    

二级缓存的问题:

  • 我们需要将实体类序列化,否则就会报错(参数readonly设置为true的话不报错。但是默认是false)

小结:

  1. 只要开启了二级缓存,在同一个命名空间下就有效
  2. 所有的数据都会先放在一级缓存中,只有当会话提交或关闭的时候,才会保存到二级缓存中。

5. 缓存原理

image-20220507200059730

6. 自定义缓存 ehcache

自定义缓存实现Cache接口即可,然后重写方法,但是重写方法中的内容需要精通数据库才可以,所以我们选择使用已经写好的缓存实现。

Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存

要在程序中使用ehcache,需要先进行导包:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

使用:

  • 在Mapper.xml中进行声明使用:

    <!--使用二级缓存-->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    
  • 类似于log4j,可以自定义单独的配置文件

    ehcache.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
             updateCheck="false">
    
        <!--
            diskstore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下;
            user.home -用户主目录
            user.dir —用户当前工作目录
            java.io.tmpdir-默认临时文件路径
        -->
        <diskStore path="./tmpdir/Tmp_EhCache"/>
        <defaultCache
                eternal="false"
                maxElementsInMemory="10000"
                overflowToDisk="false"
                diskPersistent="false"
                timeToIdleSeconds="1800"
                timeToLiveSeconds="259200"
                memoryStoreEvictionPolicy="LRU"/>
        <cache
                name="cloud_user"
                eternal="false"
                maxElementsInMemory="5000"
                overflowToDisk="false"
                diskPersistent="false"
                timeToIdleSeconds="1800"
                timeToLiveSeconds="1800"
                memoryStoreEvictionPolicy="LRU"/>
        <!--
            defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
        -->
    
        <!--
            name:缓存名称。
            maxElementsInMemory:缓存最大数目
            maxElementsonDisk:硬盘最大缓存个数。
            eternal:对象是否水久有效,但设置了,timeout将不起作用。
            overflowToDisk:是否保存到磁盘,当系统当机时
            timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永 久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
            timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
            diskpersistent:是否缓存虚拟机重启期数据 whether the disk store persists between restarts of the virtual Machine.The default value is false.
            diskSpoolBuffersizeMB:这个参数设置Diskstore(磁盘缓存)的缓存区大小。默认是30MB。每个 Cache都应该有自己的一个缓冲区。
            diskExpiryThreadintervalseconds:磁盘失效线程运行时间间隔,默认是120秒。
            memorystoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略 去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
            clearonFlush:内存数量最大时是否清除。
            memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、 LFU(最少访问次数)
            clearonFlush:内存数量最大时是否清除。
            memorystoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、 LFU(最少访问次数)。
            FIFO,first in first out,这个是大家最熟的,先进先出。
            LFU,Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
            LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间截,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
        -->
    </ehcache>
    

后续我们的缓存使用redis数据库来做缓存!K-V键值对~


十四、聊聊Spring

学到这里,我们就可以将之前的超时订单管理系统的数据库功能全部用mybatis优化一遍,顺便当做练习

接下来我们要学习的是Spring,先简单的聊一下

posted @ 2022-05-07 21:08  Laxsilence  阅读(173)  评论(1)    收藏  举报