MyBatis与MyBatis-Plus

MyBatis

简介

MyBatis 是一款优秀的 持久层框架。它支持 定制化 SQL、存储过程以及高级映射

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML注解 来配置和映射 原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为 数据库中的记录

  • 持久化

    • 持久化就是将程序的数据在 持久状态瞬时状态 转化的过程。

    • 由于内存有断电之后数据就会丢失的特点,有一些对象不能丢失,需要将其持久化。

  • 持久层

    • 完成持久化工作的代码块就是持久层。

传统的 JDBC 代码太复杂,MyBatis 简化了将数据存入数据库这一过程。MyBatis 将 sql 和代码分离,提高了可维护性;提供了映射标签,支持对象与数据库的字段关系的映射(ORM,Object Relation Mapping);提供 xml 标签,支持编写动态 sql


写一个 MyBatis 程序

搭建数据库

CREATE DATABASE IF NOT EXISTS mybatis;

USE mybatis;
DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
	id	INT		NOT NULL,
	`name`	VARCHAR(30) 	DEFAULT NULL,
	passwd 	VARCHAR(30) 	DEFAULT NULL,
	PRIMARY KEY (id)
) ENGINE = InnoDB;

INSERT INTO `user`(id, `name`, passwd) 
VALUES (1, '黄铁', '123456'),
	(2, '张三', '123456'),
	(3, '李四', '123456');
	
SELECT * FROM `user`;

新建项目

导入相关依赖
<dependencies>
    <!-- 连接mysql的驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.0.33</version>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>
    <!-- junit -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.3</version>
    </dependency>
</dependencies>

编写核心配置文件

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

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

核心配置文件的写法直接按照MyBatis官网MyBatis 3 入门的配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- mybatis 核心配置文件 -->
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    
    <!-- 每个mapper.xml都需要在该配置文件中注册-->
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>

</configuration>

编写 mybatis 工具类
/**
 * 从sqlSessionFactory获取sqlSession
 * sqlSession 完全包含了面向数据库执行SQL命令所需的所有方法
 */
public class MyBatisUtils {
    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

编写代码

实体类
public class User {
    private int id;

    private String name;

    private String passwd;

    public User () {

    }

    public User(int id, String name, String passwd) {
        this.id = id;
        this.name = name;
        this.passwd = passwd;
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPasswd() {
        return passwd;
    }

    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
    
        @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", passwd='" + passwd + '\'' +
                '}';
    }
}

DAO 接口
public interface UserDao {
    List<User> getUserList();
}

接口实现类由原来的 UserDaoImpl 转变为一个 Mapper 配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.UserDao">
	 <!--  返回值是list时,resultType填写 List中存放的对象类型  -->
    <select id="getUserList" resultType="com.hunter.pojo.User">
        select * from user
    </select>
</mapper>

测试
public class UserDaoTest {
    @Test
    public void test() {
        // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
        try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
            // 执行sql
            UserDao userDao = sqlSession.getMapper(UserDao.class);
            List<User> userList = userDao.getUserList();

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

增加 CRUD

将 UserDao 变更为 UserMapper。

  • id

    对应 namespace 中的方法名。

  • parameterType

    参数类型。

  • resultType

    sql 语句执行的返回类型。返回值类型是 List 时,resultType 填写 List 中存放的对象类型

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

<!--  对象中的属性,可以直接取出  -->
<insert id="addUser" parameterType="com.hunter.pojo.User">
    insert into user (id, name, passwd) values (#{id}, #{name}, #{passwd})
</insert>

<update id="updateUser" parameterType="com.hunter.pojo.User">
    update user set name = #{name}, passwd = #{passwd} where id = #{id}
</update>

<delete id="deleteUser" parameterType="int">
    delete from user where id = #{id}
</delete>

测试

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

@Test
public void should_success_when_delete_user() {
    // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
        // 执行sql
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        int result = userMapper.deleteUser(4);
        if (result > 0) {
            System.out.println("删除成功");
        }

        // 提交事务
        sqlSession.commit();
    }
}

用 Map 传参

如果实体类或数据库中的表,字段或者参数过多,可以考虑使用 Map:parameterType="map",传参 geng。

/**
 * 修改用户密码
 *
 * @param map 通过map传参,无需构造一个完整的user
 * @return 影响行数
 */
int updateUserPasswd(Map<String, Object> map);
<!--  parameterType 为 map 时,属性名无需和数据库表的字段一一对应  -->
<insert id="updateUserPasswd" parameterType="map">
    INSERT INTO user (id, passwd) VALUES (#{userId}, #{password});
</insert>
try(SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    Map<String, Object> map = new HashMap<>();
    map.put("userId", 1);
    map.put("password", "666666");

    userMapper.updateUserPasswd(map);
}

配置解析

详见MyBatis 3 | 配置

核心配置文件

MyBatis官方示例使用的文件名:mybatis-config.xml,可以沿用。

各标签的顺序必须如下

< configuration > (配置)

  • < properties > (属性)
  • < settings > (设置)
  • < typeAliases > (类型别名)
  • < typeHandlers > (类型处理器)
  • < objectFactory > (对象工厂)
  • < plugins > (插件)
    • mybatis-generator-core
    • mybatis-plus
    • 通用 mapper
  • < environments > (环境配置)
    • < environment > (环境变量)
      • < transactionManager > (事务管理器)
      • < dataSource > (数据源)
  • < mappers > (映射器)

环境配置 environments
  • <transactionManager>

    MyBatis 中有两种类型的事务管理器(type = “JDBC|MANAGED”)

    如果使用 Spring + MyBatis,则 没有必要配置事务管理器

  • <dataSource>

    • UNPOOLED
    • POOLED 池化数据源
    • JNDI

属性 properties

可以通过 properties 属性来实现 引用配置文件

这些属性都是 可外部配置且动态替换的,既可以在典型的 Java 属性文件中(如 db.properties),也可通过 properties 元素的子元素(property)来传递。

编写一个配置文件 db.properties

<!-- 引入外部配置文件,
    如果外部配置文件有和子元素property相同的字段,外部配置文件更优先 -->
<properties resource="db.properties">
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</properties>

类型别名 typeAliases
  • 类型别名是为 Java 类型设置一个短的名字,用于 减少类完全限定名的冗余

    <!-- 给实体类起别名 -->
    <typeAliases>
        <typeAlias type="com.hunter.pojo.User" alias="User"/>
    </typeAliases>
  • 也可以 指定一个包名,MyBatis 会在包名下搜索需要的 Java Bean,比如扫描实体类的包,它的别名就是 首字母小写的类名

    <!-- 指定包名 -->
    <typeAliases>
        <package name="com.hunter.pojo"/>
    </typeAliases>

    这种方式无法自定义别名,如果要自定义,需要在实体类上增加注解:

    import org.apache.ibatis.type.Alias;
    
    @Alias("Hello")
    public class User {...}

设置 settings
  • 可以指定 MyBatis 所用日志的具体实现,未指定时,将自动查找。

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

映射器 mappers
  • 方式一:注册 mapper.xml 文件【推荐使用】

    <!-- 每个mapper.xml都需要在该配置文件中注册-->
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
  • 方式二:使用 class 文件绑定注册

    <mappers>
        <mapper class="com.hunter.dao.UserMapper"/>
    </mappers>
    • 接口和 Mapper 配置文件 必须同名
    • 接口和 Mapper 配置文件 必须在同一个包下
  • 方式三:使用扫描包进行注册绑定

    <mappers>
        <package name="com.hunter.dao"/>
    </mappers>
    • 接口和 Mapper 配置文件 必须同名
    • 接口和 Mapper 配置文件 必须在同一个包下

生命周期和作用域

生命周期和作用域至关重要,错误的使用会导致严重的 并发问题

  • SqlSessionFactoryBuilder

    一旦创建了 SqlSessionFactory,就不再需要它,因此作为局部变量即可。

  • SqlSessionFactory

    可以理解为数据库连接池,一旦被创建,就应该在应用运行期间一直存在,没有任何理由丢弃或重新创建另一个实例,多次重建 SqlSessionFactory 被视为一种代码 坏习惯。因此最简单的就是 使用单例模式 或者 静态单例模式

  • SqlSession

    连接到连接池的 一个请求。用完之后需要马上关闭,避免资源的占用。


结果集映射 resultMap

可以解决 对象的属性名和数据库中字段名不一致 的问题。

<mapper namespace="com.hunter.dao.UserMapper">
    <!--  结果集映射  -->
    <resultMap id="UserMap" type="User">
        <!--  column 数据库中的字段,property 实体类中的属性  -->
        <result column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="passwd" property="pwd"/>
    </resultMap>

    <!--  返回值类型是List时,resultType填写List中存放的对象类型  -->
    <select id="getUserList" resultMap="UserMap">
        SELECT * FROM user;
    </select>
</mapper>

resultMap 是 Mybatis 中最强大的元素。其设计思想是,简单的语句不需要配置显式的结果映射,复杂的语句只需要描述它们的关系。但是 现实不仅仅只有这么简单的关系


日志工厂

初学者定位异常的手段,通常是 控制台输出debug,而日志定位是现实项目的成熟方式。

  • STDOUT_LOGGING 标准日志输出

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

LOG4J

Log4j 是 Apache 的开源项目,可以控制日志信息输送的目的地是控制台、文件、GUI 组件。

  • 可以控制每条日志的输出格式。
  • 通过定义每条日志信息的级别,能够更加细致地控制日志的生成过程。
  • 通过 配置文件 来灵活地配置,不需要修改应用的代码。
  1. 导包

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.20.0</version>
    </dependency>
  2. 创建 log4j.properties

    # 将等级为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/mybatis_learning.log
    # 每个log文件的大小,超过之后会生成一个新的日志,KB、MB、GB
    log4j.appender.file.MaxFileSize=1MB
    log4j.appender.file.Threshold=DEBUG
    # 输出的文件格式
    log4j.appender.file.layout=org.apache.log4j.PatternLayout
    log4j.appender.file.layout.ConversionPattern=【%p】【%d{yyyy-MM-dd hh:mm:ss}】【%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>

简单使用
  1. 在要使用 log4j 的类中,导入包 org.apache.log4j.Logger

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

    public static Logger logger = Logger.getLogger(UserMapperTest.class);
  3. 日志级别

    logger.info("进入了test log4j");
    logger.debug("进入了test log4j");
    logger.error("进入了test log4j");

分页

分页能减少数据的处理量。

方式一 使用 MySQL 中的 LIMIT 语句

SELECT prod_name
FROM products
LIMIT 5 OFFSET 4; -- 从第4行开始,取5行;等价于 LIMIT 4, 5

MySQL 检索出来的 第一行 是行 0 而不是行 1。

  1. 接口

    /**
     * 分页查询用户
     *
     * @return List<User>
     */
    List<User> getUserListByLimit(Map<String, Integer> map);
  2. Mapper.xml

    <!--  分页  -->
    <select id="getUserListByLimit" parameterType="map" resultType="user">
        SELECT *
        FROM user
        LIMIT #{startIndex}, #{pageSize};
    </select>
  3. 测试

    @Test
    public void should_success_when_get_user_list_by_limit() {
        // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
        try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
            // 执行sql
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
            Map<String, Integer> map = new HashMap<>();
            map.put("startIndex", 1);
            map.put("pageSize", 2);
            List<User> userList = userMapper.getUserListByLimit(map);
            for (User user : userList) {
                System.out.println(user);
            }
        }
    }

方式二 RowBounds

  1. 接口

    /**
     * 分页查询用户
     *
     * @return List<User>
     */
    List<User> getUserListByRowBounds();
  2. Mapper.xml

    <!--  分页  -->
    <select id="getUserListByRowBounds" resultType="user">
        SELECT *
        FROM user;
    </select>
  3. 测试

    @Test
    public void should_success_when_get_user_list_by_row_bounds() {
        // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
        try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
            RowBounds rowBounds = new RowBounds(1, 2);
            
            List<User> userList = sqlSession.selectList("com.hunter.dao.UserMapper.getUserListByRowBounds", null, rowBounds);
            for (User user : userList) {
                System.out.println(user);
            }
        }
    }

方式三 分页插件 pagehelper

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>

使用注解开发

在真正的开发中,很多时候我们会选择 面向接口编程,因为面向接口是一种 解耦 的方式,易扩展、能提高复用率。

分层开发中,上层不用关注具体的实现,大家都遵守共同的标准,使得开发变得容易,规范性更好。

  1. 注解在接口上的实现

    @Select("SELECT * FROM user")
    List<User> getUserList();
    
    @Insert("INSERT INTO user(id, name, passwd) VALUES (#{id}, #{name}, #{passwd})")
    int addUser(User user);
    
    @Update("UPDATE user SET name=#{name}, passwd=#{passwd} WHERE id=#{id}")
    int updateUser(User user);
    
    @Delete("DELETE FROM user WHERE id=#{uid}")
    int deleteUser(@Param("uid") int id);
  2. 需要在核心配置文件中绑定接口

    <mappers>
        <mapper class="com.hunter.dao.UserMapper"/>
    </mappers>

关于 @Param() 注解

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

复杂查询

搭建数据库表

CREATE TABLE `teacher` (
    `id` INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=INNODB;

INSERT INTO teacher(`id`, `name`) VALUES (1, 'H老师');

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;

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');

测试环境搭建

  1. 导入 lombok
  2. 新建实体类 Teacher、Student
  3. 建立 Mapper 接口
  4. 建立 Mapper.xml
  5. 在核心配置文件中绑定注册 Mapper.xml 文件(方式很多)
  6. 测试查询

多对一处理

举例:多个学生归属一个老师(如班主任概念)


方式一 嵌套查询处理
<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.StudentMapper">
    <!--  结果集映射  -->
    <resultMap id="StudentWithTeacherName" type="student">
        <!--  column 数据库中的字段,property 实体类中的属性  -->
        <result column="id" property="id"/>
        <result column="name" property="name"/>
        <!-- 复杂的属性需要单独处理:
            对象:association
            集合:collection
        -->
        <!--
            column:在查询的结果集中,用该列值作为查询条件执行下一条SQL语句
            select: 下一条执行的语句
            javaType:将执行的结果集封装成某个Java类型
         -->
        <association property="teacher" column="tid" select="getTeacher" javaType="teacher"/>
    </resultMap>

    <!-- 使用结果集映射resultMap 映射数据库的字段和实体类的属性 -->
    <select id="getStudentList" resultMap="StudentWithTeacherName">
        SELECT *
        FROM student;
    </select>

    <select id="getTeacher" resultType="teacher">
        SELECT *
        FROM teacher
        WHERE id = #{tid};
    </select>
</mapper>

方式二 联表查询处理
<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.StudentMapper">
    <!--  结果集映射  -->
    <resultMap id="StudentWithTeacherName" type="student">
        <!--  column 数据库中的字段,property 实体类中的属性  -->
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <!-- 复杂的属性需要单独处理:
            对象:association
            集合:collection
        -->
        <!-- javaType:将执行的结果集封装成某个Java类型 -->
        <association property="teacher" javaType="teacher">
            <result property="id" column="tid"/>
            <result property="name" column="tname"/>
        </association>
    </resultMap>

    <!-- 使用结果集映射resultMap 映射数据库的字段和实体类的属性 -->
    <select id="getStudentList" resultMap="StudentWithTeacherName">
        SELECT s.id AS sid, s.name AS sname, t.id AS tid, t.name AS tname
        FROM student AS s
            INNER JOIN teacher AS t
            ON s.tid = t.id;
    </select>
</mapper>

一对多处理

举例:一个老师拥有多个学生

联表查询
<mapper namespace="com.hunter.dao.TeacherMapper">
    <select id="getTeacher" resultMap="teacherWithStudents">
        SELECT s.id AS sid, s.name AS sname, t.id AS tid, t.name AS tname
        FROM student AS s
            INNER JOIN teacher AS t
                ON s.tid = t.id AND t.id = #{tid};
    </select>

    <!--  结果集映射  -->
    <resultMap id="teacherWithStudents" type="teacher">
        <!--  column 数据库中的字段,property 实体类中的属性  -->
        <result property="id" column="tid"/>
        <result property="name" column="tname"/>
        <!-- 复杂的属性需要单独处理:
            对象:association
            集合:collection
        -->
        <!-- 集合中的泛型信息,使用ofType获取 -->
        <collection property="studentList" ofType="student">
            <result property="id" column="sid"/>
            <result property="name" column="sname"/>
            <result property="tid" column="tid"/>
        </collection>
    </resultMap>
</mapper>

动态 SQL

动态 SQL 指 根据不同的条件,生成不同的 SQL 语句。其实动态 SQL 就是在拼接 SQL 语句,只要保证 SQL 的正确性,按照 SQL 的格式去排列组合 就可以了。

搭建数据库表

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;

测试环境搭建

  1. 导入 lombok

  2. 新建实体类 Blog

  3. 建立 Mapper 接口

  4. 建立 Mapper.xml

  5. 在核心配置文件中绑定注册 class 文件 BlogMapper(方式很多)

  6. 开启自动提交事务

    public static SqlSession getSqlSession() {
        // autoCommit:自动开启事务
        return sqlSessionFactory.openSession(true);
    }
  7. 初始化数据库表数据

    public class BlogMapperTest {
        @Test
        public void initBlog() {
            try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
                BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    
                mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Mybatis学习", "Hunter", new Date(), 0));
                mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Java学习", "Hunter", new Date(), 0));
                mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Spring学习", "Hunter", new Date(), 0));
                mapper.addBlog(generateNewBlog(IdUtils.generateId(), "微服务学习", "Hunter", new Date(), 0));
            }
        }
    
        private Blog generateNewBlog(String id, String title, String author, Date date, int views) {
            Blog blog = new Blog();
            blog.setId(id);
            blog.setTitle(title);
            blog.setAuthor(author);
            blog.setCreateTime(date);
            blog.setViews(views);
            return blog;
        }
    }

if 语句

<select id="queryBlogIf" parameterType="map" resultType="blog">
    SELECT *
    FROM blog
    <!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
    <where>
        <if test="title != null">
            AND title = #{title}
        </if>
        <if test="author != null">
            AND author = #{author}
        </if>
    </where>
</select>

choose, when, otherwise 语句

<select id="queryBlogChoose" parameterType="map" resultType="blog">
    SELECT *
    FROM blog
    <!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
    <where>
        <choose>
            <when test="title != null">
                AND title = #{title}
            </when>
            <when test="author != null">
                AND author = #{author}
            </when>
            <otherwise>
                AND views = #{views}
            </otherwise>
        </choose>
    </where>
</select>

set 语句

<update id="updateBlog" parameterType="map">
    UPDATE blog
    <!-- set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。-->
    <set>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="author != null">
            author = #{author},
        </if>
    </set>
    WHERE id = #{id}
</update>

sql 片段

使用 sql 标签 抽取出公共的部分,在使用的地方使用 include 标签引用即可。

  • 最好 基于单表来定义 sql 片段
  • 不要存在 where 标签
<sql id="if-title-author">
    <if test="title != null">
        AND title = #{title}
    </if>
    <if test="author != null">
        AND author = #{author}
    </if>
</sql>

<select id="queryBlogIf" parameterType="map" resultType="blog">
    SELECT *
    FROM blog
    <!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
    <where>
        <include refid="if-title-author"></include>
    </where>
</select>

foreach

<!-- SELECT * FROM blog WHERE (id = 1 OR id = 2 OR id = 3) -->
<select id="queryBlogForEach" parameterType="map" resultType="blog">
    SELECT *
    FROM blog
    <where>
        <foreach collection="ids" item="id" open="(" separator="OR" close=")">
            id = #{id}
        </foreach>
    </where>
</select>

缓存

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

经常查询并且不经常改变的数据适合缓存。

MyBatis 中定义了两级缓存:一级缓存二级缓存默认情况下,只开启了一级缓存SqlSession 级别 的缓存,也称为本地缓存)。二级缓存是 基于 namespace 级别 的缓存。为了提高扩展性,MyBatis 定义了缓存接口 Cache,可以通过实现 Cache 接口来自定义二级缓存


一级缓存

一级缓存,也叫本地缓存,是 SqlSession 级别 的缓存,它 就是一个 map默认开启

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

缓存存在的例子:

@Test
public void test_cache() {
    // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
        // 执行sql
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUserById(1);
        System.out.println(user);
        System.out.println("=============================================================");
        user = userMapper.getUserById(1);
        System.out.println(user);
    }
}

从日志可以看出,只有一次访问数据库的记录。image-20230524222922360


缓存失效的情况:

  1. 对数据库进行增删改操作,由于可能会改变原来的数据,缓存会刷新。

  2. 手动清理

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

例子:

@Test
public void test_cache() {
    // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
        // 执行sql
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUserById(1);
        System.out.println(user);
        System.out.println("=============================================================");
        // 增加操作
        user = new User(4, "王五", "123456");
        userMapper.addUser(user);

        System.out.println("=============================================================");
        // 重新访问数据库(确保获取数据变动后的内容),然后刷新缓存
        user = userMapper.getUserById(1);
        System.out.println(user);
    }
}

image-20230524222548553


二级缓存

二级缓存,也叫 全局缓存,是 namespace 级别的缓存,一个命名空间,对应一个二级缓存

  • 工作机制:
    • 一个会话查询一条数据,这个数据就会被放在当前会话的 一级 缓存中
    • 如果当前会话关闭,一级缓存中的数据被保存到二级缓存中
    • 新的会话查询信息,就从二级缓存中获取内容
    • 不同的 mapper 查出的数据会放在自己对应的缓存(map)中
使用步骤
  1. 在 mybatis 的核心配置文件中,开启全局缓存

    <!-- 显式地开启全局缓存(默认已开启) -->
    <setting name="cacheEnabled" value="true"/>
  2. 要使用二级缓存的 Mapper 中 开启缓存

    <!-- 使用二级缓存 -->
    <cache/>
    
    <!-- 也可自定义参数 -->
    <cache eviction="FIFO" 
           flushInterval="60000" 
           size="512"
           readOnly="true"/>

    如果 readOnly 参数没有设置,需要将实体类序列化(implements Serializable


获取数据顺序

  1. 先看二级缓存
  2. 再看一级缓存
  3. 查询数据库

自定义缓存 Ehcache

  1. 导包

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.3</version>
    </dependency>
  2. 在 mapper 中指定自定义的缓存实现

    <!-- 使用EhcacheCache -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
  3. 搭配相应的配置文件 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 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"/>
    </ehcache>

但是目前都用 Redis 来做缓存,本章了解一下。