首页 > 技术文章 > 20201122 MyBatis - 拉勾教育 - 流星

huangwenjie 2020-11-22 15:55 原文

环境信息

  • 最新 MyBatis 版本:3.5.6 - 2021年2月18日
  • 笔记 MyBatis 版本:3.4.5

第一部分:自定义持久层框架(类 MyBatis)

第二部分:MyBatis 相关概念

  • ORM 全称 Object/Relation Mapping:对象-关系映射

  • MyBatis 是一款优秀的 基于 ORM 的半自动轻量级持久层框架

  • MyBatis 历史

    原是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移到了 google code,随着开发团队转投Google Code旗下,ibatis3.x正式更名为MyBatis ,代码于2013年11月迁移到Github。
    
    iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
    
  • 四大组件

    • Executor
    • StatementHandler
    • ParameterHandler
    • ResultSetHandler

第三部分:MyBatis基本应用

搭建 MyBatis 环境

  1. 准备数据,在 MySQL 中执行脚本

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(50) DEFAULT NULL,
      `password` varchar(50) DEFAULT NULL,
      `birthday` varchar(50) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES ('1', 'lucy', '123', '2019-12-12');
    INSERT INTO `user` VALUES ('2', 'tom', '123', '2019-12-12');
    
  2. pom.xml,Maven 依赖

    • 使用 MySQL 数据库
    • 使用 Lombok 简化代码
    • 使用 SLF4J+Logback 来打印日志
    • 使用 JUnit 来进行单元测试
    <dependencies>
        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
    
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.17</version>
        </dependency>
    
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>compile</scope>
        </dependency>
    
        <!-- Slf4j + Logback -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    
        <!-- JUnit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  3. logback.xml,日志配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!-- 为单独的包配置日志级别 -->
        <logger name="com.lagou.mybatis3.dao" level="trace"></logger>
    
        <!-- root 日志级别 -->
        <root level="info">
            <appender-ref ref="STDOUT"/>
        </root>
    </configuration>
    
  4. sqlMapConfig.xml,MyBatis 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>
    
        <!--加载外部的properties文件-->
        <properties resource="jdbc.properties"></properties>
    
        <!--给实体类的全限定类名给别名-->
        <typeAliases>
            <!--给单独的实体起别名-->
            <!--  <typeAlias type="com.lagou.pojo.User" alias="user"></typeAlias>-->
            <!--批量起别名:该包下所有的类的本身的类名:别名还不区分大小写-->
            <package name="com.lagou.mybatis3.pojo"/>
        </typeAliases>
    
        <!--environments:运行环境-->
        <environments default="development">
            <environment id="development">
                <!--当前事务交由JDBC进行管理-->
                <transactionManager type="JDBC"></transactionManager>
                <!--当前使用mybatis提供的连接池-->
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.username}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
    
        <!--引入映射配置文件-->
        <mappers>
            <mapper resource="UserMapper.xml"></mapper>
        </mappers>
    
    
    </configuration>
    
  5. UserMapper.xml,MyBatis XML 映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.lagou.mybatis3.dao.IUserDao">
        <select id="findAll" resultType="com.lagou.mybatis3.pojo.User">
            select * from user
        </select>
    </mapper>
    
  6. User.java,实体类

    package com.lagou.mybatis3.pojo;
    
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class User {
        private Integer id;
        private String username;
        private String password;
        private Date birthday;
    }
    
  7. IUserDao.java,接口定义

    package com.lagou.mybatis3.dao;
    
    import com.lagou.mybatis3.pojo.User;
    
    import java.util.List;
    
    public interface IUserDao {
        List<User> findAll();
    }
    
  8. BaseTest.java,测试类

    /**
     * 测试 MyBatis 基础功能
     */
    @Slf4j
    public class BaseTest {
        private SqlSession sqlSession = null;
    
        @Before
        public void before() throws IOException {
            InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
    
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStreamReader);
            // autoCommit 默认为 false,不自动提交事务
            sqlSession = sqlSessionFactory.openSession();
        }
    
        @After
        public void after() {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    
        @Test
        public void testSelectList() {
            // 方式一:使用 MyBatis 方法
            List<User> userList = sqlSession.selectList("com.lagou.mybatis3.dao.IUserDao.findAll");
            for (User user : userList) {
                log.info("user :: {}", user);
            }
    
            // 方式二:使用 Mapper 接口
            IUserDao userDao = sqlSession.getMapper(IUserDao.class);
            List<User> userList2 = userDao.findAll();
            for (User user2 : userList2) {
                log.info("user2 == {}", user2);
            }
        }
    
    }
    

第四部分:MyBatis 配置文件深入

MyBatis XML 配置文件,sqlMapConfig.xml

官网参考

  • 文件标签之间有严格的顺序,不遵守会报错

  • environments 标签

    数据库环境的配置,支持多环境配置,使用以下方式指定创建环境,如果不指定,使用 default 属性指定的环境

    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
    
  • mapper 标签

    该标签的作用是加载映射的,加载方式有如下几种:

    1. 使用相对于类路径的资源引用,例如:
    <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
    
    2. 使用映射器接口实现类的完全限定类名,例如:
    <mapper class="org.mybatis.builder.AuthorMapper"/>
    
    3. 使用完全限定资源定位符(URL),例如:
    <mapper url="file:///var/mappers/AuthorMapper.xml"/>
    
    4. 将包内的映射器接口实现全部注册为映射器,例如:
    <package name="org.mybatis.builder"/>
    
  • properties 标签

    实际开发中,习惯将数据源的配置信息单独抽取成一个 properties 文件,该标签可以加载额外配置的
    properties 文件

  • typeAliases 标签

    配置类型别名,MyBatis 框架已经为我们设置好的一些常用的类型的别名:

    官网参考

    源码参考:org.apache.ibatis.type.TypeAliasRegistry#TypeAliasRegistry

MyBatis XML 映射文件,UserMapper.xml

动态 SQL

官网参考

MyBatis 支持的动态 SQL:

  • if
  • choose、when、otherwise
  • trim、where、set
  • foreach
  • script
  • bind
  • 多数据库支持
    • _databaseId
  • 动态 SQL 中的插入脚本语言

源码参考:org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#nodeHandlers

XML 映射文件

官网参考

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。
SQL 片段抽取,sql 标签用法
<!--抽取sql片段-->
<sql id="selectUser">
	select * from user
</sql>

<!--查询用户-->
<select id="findAll" resultType="uSeR">
    <include refid="selectUser"></include>
</select>

第五部分:MyBatis 复杂映射开发

准备工作

准备数据

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  `password` varchar(50) DEFAULT NULL,
  `birthday` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'lucy', '123', '2019-12-12');
INSERT INTO `user` VALUES ('2', 'tom', '123', '2019-12-12');

DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ordertime` varchar(255) DEFAULT NULL,
  `total` double DEFAULT NULL,
  `uid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uid` (`uid`),
  CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of orders
-- ----------------------------
INSERT INTO `orders` VALUES ('1', '2019-12-12', '3000', '1');
INSERT INTO `orders` VALUES ('2', '2019-12-12', '4000', '1');
INSERT INTO `orders` VALUES ('3', '2019-12-12', '5000', '2');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(255) DEFAULT NULL,
  `roleDesc` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'CTO', 'CTO');
INSERT INTO `sys_role` VALUES ('2', 'CEO', 'CEO');

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `userid` int(11) NOT NULL,
  `roleid` int(11) NOT NULL,
  PRIMARY KEY (`userid`,`roleid`),
  KEY `roleid` (`roleid`),
  CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `sys_role` (`id`),
  CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '1');
INSERT INTO `sys_user_role` VALUES ('1', '2');
INSERT INTO `sys_user_role` VALUES ('2', '2');

实体类

User.java

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private Date birthday;

    //代表当前用户具备哪些订单
    private List<Order> orderList;

    //代表当前用户具备哪些角色
    private List<Role> roleList;
}

Order.java

@Data
public class Order {
    private Integer id;
    private Date ordertime;
    private Double total;

    //代表当前订单从属于哪一个客户
    private User user;
}

Role.java

@Data
public class Role {
    private int id;
    private String rolename;
}

一对一查询

<resultMap id="orderResultMap" type="com.lagou.mybatis3.pojo.Order">
    <result property="id" column="id"></result>
    <result property="ordertime" column="ordertime"></result>
    <result property="total" column="total"></result>
    <association property="user" javaType="com.lagou.mybatis3.pojo.User">
        <result property="id" column="uid"></result>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <result property="birthday" column="birthday"></result>
    </association>
</resultMap>

<select id="findAll" resultMap="orderResultMap">
    select * from orders o,user u where o.uid=u.id
</select>
    @Test
    public void testOne2One() {
        IOrderDao orderDao = sqlSession.getMapper(IOrderDao.class);
        List<Order> orderList = orderDao.findAll();
        for (Order order : orderList) {
            log.info("order == {}", order);
        }
    }

一对多查询

    <resultMap id="userResultMap" type="com.lagou.mybatis3.pojo.User">
        <result property="id" column="id"></result>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <result property="birthday" column="birthday"></result>
        <collection property="orderList" ofType="com.lagou.mybatis3.pojo.Order">
            <result property="id" column="oid"></result>
            <result property="ordertime" column="ordertime"></result>
            <result property="total" column="total"></result>
        </collection>
    </resultMap>

    <select id="findAllWithOrders" resultMap="userResultMap">
        select *, o.id oid from user u inner join orders o on u.id = o.uid;
    </select>
    @Test
    public void testOne2Many() {
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        List<User> userList = userDao.findAllWithOrders();
        for (User user : userList) {
            System.out.println(user);
        }
    }

多对多查询

    <select id="findAllWithRoles" resultMap="userWithRolesResultMap">
        select * from user u
        inner join sys_user_role ur on u.id = ur.userid
        inner join sys_role r  on r.id = ur.roleid
    </select>
	@Test
    public void testMany2Many() {
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        List<User> userList = userDao.findAllWithRoles();
        for (User user : userList) {
            System.out.println(user);
        }
    }

延迟加载

  • 延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
  • 在多表中:
    • 一对多,多对多:通常情况下采用延迟加载
    • 一对一(多对一):通常情况下采用立即加载
  • 延迟加载是基于嵌套查询来实现的

XML 配置延迟加载

  • 注解配置同理
<settings>
    <!--开启全局延迟加载功能-->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>


<!-- 开启一对多 延迟加载 -->
<resultMap id="userMap" type="user">
    <id column="id" property="id"></id>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="birthday" property="birthday"></result>
    <!--
    fetchType="lazy" 懒加载策略
    fetchType="eager" 立即加载策略
    -->
    <collection property="orderList" ofType="order" column="id" select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
    </collection>
</resultMap>

第六部分:MyBatis 注解开发

MyBatis 的常用注解

  • @Insert:实现新增
  • @Update:实现更新
  • @Delete:实现删除
  • @Select:实现查询
  • @Result:实现结果集封装
  • @Results:可以与@Result 一起使用,封装多个结果集
  • @One:实现一对一结果集封装
  • @Many:实现一对多结果集封装

复杂查询

UserAnnoDao.java

package com.lagou.mybatis3.anno;

import com.lagou.mybatis3.pojo.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserAnnoDao {

    @Select("select * from user where id=#{id}")
    User findById(int id);

    @Select("select * from user")
    @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "username",column = "username"),
            @Result(property = "password",column = "password"),
            @Result(property = "birthday",column = "birthday"),
            @Result(property = "orderList",column = "id", javaType = List.class,
                    many = @Many(select = "com.lagou.mybatis3.anno.OrderAnnoDao.findByUid"))
    })
    List<User> findAllWithOrders();

    @Select("select * from user")
    @Results({
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "username",column = "username"),
            @Result(property = "password",column = "password"),
            @Result(property = "birthday",column = "birthday"),
            @Result(property = "roleList",column = "id", javaType = List.class,
                    many = @Many(select = "com.lagou.mybatis3.anno.RoleAnnoDao.findByUid")
            )
    })
    List<User> findAllWithRoles();
}

OrderAnnoDao.java

public interface OrderAnnoDao {
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "ordertime", column = "ordertime"),
            @Result(property = "total", column = "total"),
            @Result(property = "user", column = "uid", javaType = User.class,
                    one = @One(select = "com.lagou.mybatis3.anno.UserAnnoDao.findById")
            )
    })
    @Select("select * from orders")
    List<Order> findAllWithUser();

    @Select("select * from orders where uid=#{uid}")
    List<Order> findByUid(int uid);
}

RoleAnnoDao.java

public interface RoleAnnoDao {
    @Select("select * from sys_role r,sys_user_role ur where r.id=ur.roleid and ur.userid=#{uid}")
    List<Role> findByUid(int uid);
}

测试代码:

@Test
public void testRelated() {
    UserAnnoDao userAnnoDao = sqlSession.getMapper(UserAnnoDao.class);
    OrderAnnoDao orderAnnoDao = sqlSession.getMapper(OrderAnnoDao.class);
    // 一对一
    List<Order> orderList = orderAnnoDao.findAllWithUser();
    for (Order order : orderList) {
        System.out.println("order :: " + order);
    }

    // 一对多
    List<User> userList = userAnnoDao.findAllWithOrders();
    for (User user : userList) {
        System.out.println("user-order :: " + user);
    }

    // 多对多
    List<User> userRoleList = userAnnoDao.findAllWithRoles();
    for (User user : userRoleList) {
        System.out.println("user-role :: " + user);
    }
}

第七部分:MyBatis 缓存

  • 一级缓存是基于 SqlSession 的,一级缓存默认开启
  • 二级缓存是基于 Mapper 文件的 namespace,二级缓存需要手动开启
  • 一级缓存的实现是 SqlSession 级别,二级缓存是 MappedStatement 级别
  • 缓存都是基于 org.apache.ibatis.cache.Cache 接口
    • PerpetualCache 是 MyBatis 缓存的默认实现
  • 可以在 MyBatis 配置文件和 Mapper 文件中设置 useCacheflushCache,控制是否使用缓存

一级缓存

  • 一级缓存也叫做 查询缓存

代码示例

  • 相同 SqlSession 的两次相同查询,第二次从缓存中取值
  • 相同 SqlSession 的两次相同查询中间执行了 update 操作,第二次查询从数据库中取值
@Slf4j
public class CacheLevelOneTest {

    private SqlSessionFactory sqlSessionFactory = null;
    private SqlSession sqlSession = null;

    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);

        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStreamReader);
        // autoCommit 默认为 false,不自动提交事务
        // sqlSession = sqlSessionFactory.openSession(true);
    }

    @After
    public void after() {
        if (sqlSession != null) {
            sqlSession.close();
        }

    }

    /**
     * 测试两次相同查询间的缓存
     */
    @Test
    public void testCache() {
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUserDao userMapper = sqlSession.getMapper(IUserDao.class);

        User userCondition = new User();
        userCondition.setId(2);
        //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        User user = userMapper.findByCondition(userCondition);
        System.out.println("user :: " + user);

        //第二次查询,由于是同一个sqlSession,会在缓存中查询结果
        //如果有,则直接从缓存中取出来,不和数据库进行交互
        User user2 = userMapper.findByCondition(userCondition);
        System.out.println("user2 :: " + user2);
    }

    /**
     * 测试一次 update 间隔两次相同查询间的缓存
     */
    @Test
    public void testCacheAfterUpdate() {
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUserDao userMapper = sqlSession.getMapper(IUserDao.class);

        User userCondition = new User();
        userCondition.setId(2);
        //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        User user = userMapper.findByCondition(userCondition);
        System.out.println("user :: " + user);

        // 更新
        user.setUsername("cc");
        userMapper.updateUser(user);

        //第二次查询,由于 update 清空缓存信息
        //此次查询也会发出sql语句
        User user2 = userMapper.findByCondition(userCondition);
        System.out.println("user2 :: " + user2);
    }

}

源码分析

一级缓存的底层实现:

  • org.apache.ibatis.session.defaults.DefaultSqlSession#executor
    • CachingExecutor#delegate
      • org.apache.ibatis.executor.BaseExecutor#localCache
        • org.apache.ibatis.cache.impl.PerpetualCache#cache
          • private Map<Object, Object> cache = new HashMap<Object, Object>();

查询时创建 CacheKey :

  • org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
    • org.apache.ibatis.executor.BaseExecutor#createCacheKey

CacheKey 中包含内容,以 findByCondition 为例:

1. MappedStatement#id
	com.lagou.mybatis3.dao.IUserDao.findByCondition
2. RowBounds#offset
	0
3. RowBounds#limit
   2147483647 (Integer.MAX_VALUE)
4. BoundSql#sql
   select * from user WHERE  id = ?
5. BoundSql#parameterMappings#value
   sql 映射的参数值,可能有 0-n 个
6. Environment#id
   development

从缓存中根据 CacheKey 获取缓存值:

  • org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
    • list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

将查询结果存入缓存:

  • org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

两次查询中间使用一次 update 会清空缓存的原因:

  • sqlSession 执行插入、更新、删除操作后会清空缓存
    • DefaultSqlSession#dirty 在执行 DefaultSqlSession#update() 时会设为 true,导致缓存清空
  • sqlSession 执行提交、回退操作后会清空缓存
  • org.apache.ibatis.executor.BaseExecutor#clearLocalCache
    • org.apache.ibatis.executor.BaseExecutor#update
    • org.apache.ibatis.executor.BaseExecutor#commit
    • org.apache.ibatis.executor.BaseExecutor#rollback

二级缓存

  • 如果两个 mapper 的 namespace 相同,即使是两个不同的 mapper ,这两个 mapper 中执行 sql 查询到的数据也将存在相同的二级缓存区域中

  • 可以通过 <cache> 标签的 type 属性来配置缓存实现

开启二级缓存:

1. 在 sqlMapConfig.xml 中,默认为 true,可省略:
<settings>
	<setting name="cacheEnabled" value="true"/>
</settings>

2. 在 Mapper 文件中:
<cache></cache>

3. 查询的结果类型需要实现序列化接口,java.io.Serializable,因为二级缓存可以存储在多种介质中

4. 运行时,控制台打印如下日志,说明同一个 namespace 对应的二级缓存的击中率是 0.5,并不说明这次查询使用了缓存;没有打印出来 sql 查询语句说明使用到了二级缓存;
19:50:09.147 [main] DEBUG com.lagou.mybatis3.dao.IUserDao - Cache Hit Ratio [com.lagou.mybatis3.dao.IUserDao]: 0.5

代码示例

/**
 * 测试 MyBatis 二级缓存
 */
@Slf4j
public class CacheLevelTwoTest {

    private SqlSessionFactory sqlSessionFactory = null;
    private SqlSession sqlSession = null;

    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);

        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStreamReader);
        // autoCommit 默认为 false,不自动提交事务
        // sqlSession = sqlSessionFactory.openSession(true);
    }

    @After
    public void after() {
        if (sqlSession != null) {
            sqlSession.close();
        }

    }

    /**
     * 测试两次相同查询间的缓存
     */
    @Test
    public void testCache() {
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();

        IUserDao userMapper1 = sqlSession1.getMapper(IUserDao.class);
        IUserDao userMapper2 = sqlSession2.getMapper(IUserDao.class);
        IUserDao userMapper3 = sqlSession3.getMapper(IUserDao.class);

        User userCondition = new User();
        userCondition.setId(2);

        //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
        User user = userMapper1.findByCondition(userCondition);
        System.out.println("user :: " + user);

        // 清空一级缓存
        // sqlSession1.clearCache();
        sqlSession1.close();

        //第二次查询,由于是同一个sqlSession,会在缓存中查询结果
        //如果有,则直接从缓存中取出来,不和数据库进行交互
        User user2 = userMapper2.findByCondition(userCondition);
        System.out.println("user2 :: " + user2);

        userCondition.setId(3);
        User user3 = userMapper2.findByCondition(userCondition);
        System.out.println("user3 :: " + user3);
    }


}

源码分析

二级缓存底层实现:

  • CachingExecutor#tcm
    • TransactionalCacheManager#transactionalCaches
      • MappedStatement#cache
      • TransactionalCache#delegate
      • TransactionalCache#entriesToAddOnCommit
      • TransactionalCache#entriesMissedInCache

二级缓存中存、取值:

  • org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

注意:

  • 二级缓存中存、取值时,存和取放在不同的对象中,需要执行 tcm.commit(); 才会同步
    • 存在 TransactionalCache#entriesToAddOnCommitTransactionalCache#entriesMissedInCache
    • 取从 TransactionalCache#delegate
    • sqlSession#commitsqlSession#close 会触发 tcm.commit();

二级缓存是否失效由 DefaultSqlSession#dirty 控制,执行 DefaultSqlSession#update 时设为 true,导致二级缓存失效。

补充

  • 默认缓存开启,不开启缓存时,执行器是 SimpleExecutor,开启后是 CachingExecutor 装饰的 SimpleExecutor

  • org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)

  • 一级缓存和二级缓存的生命周期分别是:

    • 一级缓存的生命周期是会话级别,因为一级缓存是存在 Sqlsession 的成员变量 Executor 的成员变量 localCache 中的。
    • 二级缓存的生命周期是整个应用级别,因为二级缓存是存在Configuration对象中,而这个对象在应用启动后一直存在
  • 同时配置一级缓存和二级缓存后,先查询哪个缓存?

    先查询二级缓存再查询一级缓存,因为一级缓存的实现在 BaseExecutor,而二级缓存的实现在CachingExecutorCachingExecutorBaseExecutor 的装饰器

二级缓存使用 Redis 实现分布式

  1. 添加依赖

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-redis</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
    
  2. 修改 <cache> 的 type 属性

    <cache type="org.mybatis.caches.redis.RedisCache" />
    
  3. 测试,验证,执行测试代码后查看 Redis,发现对象被存入了 Redis

第八部分:MyBatis 插件

官方文档

插件介绍

MyBatis 作为一个应用广泛的优秀的 ORM 开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插件扩展机制。MyBatis 对持久层的操作就是借助于四大核心对象。MyBatis 支持用插件对四大核心对象进行拦截,对 MyBatis 来说插件就是 拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理 实现的,换句话说,MyBatis 中的四大对象都是代理对象。

img

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 插件接口 - Interceptor

  • intercept 方法,插件的核心方法
    • plugin 方法代理,且 @Intercepts 配置,拦截方法被调用时执行
  • plugin 方法,生成 target 的代理对象
    • 生成四大组件时都会调用,可以根据 target 类型判断是否代理
  • setProperties 方法,传递插件所需参数
    • 解析 sqlMapConfig.xml 时调用,只调用一次

在四大对象创建的时候:

  • 每个创建出来的对象不是直接返回的,而是 interceptorChain.pluginAll(parameterHandler);
  • 获取到所有的 Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target); 返回 target 包装后的对象
  • 插件机制,我们可以使用插件为目标对象创建一个代理对象;插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;

自定义插件

插件类:

// 可以定义多个@Signature对多个地方拦截,都用这个拦截器
@Intercepts({
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}),
        @Signature(type = Executor.class,
                method = "clearLocalCache",
                args = {})
})
@Slf4j
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增强逻辑
        System.out.println("对方法进行了增强....");
        return invocation.proceed(); //执行原方法
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            System.out.println("将要包装的目标对象:" + target);
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 获取配置文件的属性
     * 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:" + properties);
    }
}

增加配置:

<plugins>
    <plugin interceptor="com.lagou.mybatis3.plugin.MyPlugin">
        <!--配置参数-->
        <property name="name" value="Bob"/>
    </plugin>
</plugins>

测试方法:

@Test
public void testSelectList() {
    List<User> userList = sqlSession.selectList("com.lagou.mybatis3.dao.IUserDao.findAll");
    for (User user : userList) {
        log.debug("user :: ", user);
    }
    sqlSession.clearCache();
}

PageHelper 分页插件

20200115 PageHelper

MyBatis 自带分页

使用 org.apache.ibatis.session.RowBounds 作为入参实现分页

  1. Mapper 接口方法:

    List<User> findAll(RowBounds rowBounds);
    
  2. 测试代码:

    @Test
    public void testRowBounds() {
        IUserDao mapper = sqlSession.getMapper(IUserDao.class);
        // 查询前两行
        List<User> userList = mapper.findAll(new RowBounds(0, 2));
        for (User user : userList) {
            log.debug("user :: ", user);
        }
    }
    

PageHelper 分页插件使用

  1. 增加依赖

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>3.7.5</version>
    </dependency>
    
  2. 增加配置

    <plugin interceptor="com.github.pagehelper.PageHelper">
        <!--指定方言-->
        <property name="dialect" value="mysql"/>
    </plugin>
    
  3. 测试使用

    @Test
    public void testPageHelper() {
        // 设置分页参数,查询第 1 页,每页 2 行
        PageHelper.startPage(1, 2);
        // 返回的是 com.github.pagehelper.Page 对象
        List<User> userList = sqlSession.selectList("com.lagou.mybatis3.dao.IUserDao.findAll");
        for (User user : userList) {
            log.debug("user :: ", user);
        }
    
        //其他分页的数据
        PageInfo<User> pageInfo = new PageInfo<>(userList);
    
        // Page ::Page{pageNum=1, pageSize=2, startRow=0, endRow=2, total=10, pages=5, reasonable=false, pageSizeZero=false}
        System.out.println("Page ::" + userList);
        // PageInfo ::PageInfo{pageNum=1, pageSize=2, size=2, startRow=1, endRow=2, total=10, pages=5, list=Page{pageNum=1, pageSize=2, startRow=0, endRow=2, total=10, pages=5, reasonable=false, pageSizeZero=false}, firstPage=1, prePage=0, nextPage=2, lastPage=5, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=8, navigatepageNums=[1, 2, 3, 4, 5]}
        System.out.println("PageInfo ::" + pageInfo);
    }
    

通用 Mapper

GitHub 地址

  1. 增加依赖

    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper</artifactId>
        <version>3.1.2</version>
    </dependency>
    
  2. 增加配置

    <!--如果有 PageHelper 分页插件,要排在通用mapper之前,通用 Mapper-->
    <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
        <!-- 通用Mapper接口,多个通用接口用逗号隔开 -->
        <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
    </plugin>
    
  3. 实体类配置

    @Table(name = "user")
    @Data
    public class TUser {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String username;
    }
    
  4. 通用 Mapper

    public interface ITUserDao extends Mapper<TUser> {
    }
    
  5. 测试使用

    	@Test
        public void testMapper() {
            ITUserDao userMapper = sqlSession.getMapper(ITUserDao.class);
            TUser user = new TUser();
            user.setId(14);
    
            //(1)mapper基础接口
            //select 接口
            TUser user1 = userMapper.selectOne(user); //根据实体中的属性进行查询,只能有 — 个返回值
            List<TUser> users = userMapper.select(null); //查询全部结果
            userMapper.selectByPrimaryKey(1); //根据主键字段进行查询,方法参数必须包含完 整 的主键属性,查询条件使用等号
            userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使用等号
    
            // insert 接口
            int insert = userMapper.insert(user); //保存一个实体,null值也会保存,不会使 用数据库默认值
            // int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存, 会 使用数据库默认值
    
            // update 接口
            int i1 = userMapper.updateByPrimaryKey(user);//根据主键更新实体全部字段, null值会被更新
    
            // delete 接口
            int delete = userMapper.delete(user); //根据实体属性作为条件进行删除,查询条件 使用等号
            userMapper.deleteByPrimaryKey(14); //根据主键字段进行删除,方法参数必须包含完 整 的主键属性
    
            //(2)example方法
            Example example = new Example(User.class);
            example.createCriteria().andEqualTo("id", 1);
            example.createCriteria().andLike("username", "lucy");
    
            //自定义查询
            List<TUser> users1 = userMapper.selectByExample(example);
        }
    

第九部分:MyBatis 架构原理

架构设计

img

我们把 MyBatis 的功能架构分为三层:

  • API 接口层:提供给外部使用的接口 API,开发人员通过这些本地 API 来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

    MyBatis 和数据库的交互有两种方式:

    • 使用传统的 MyBatis 提供的 API;
    • 使用 Mapper 代理的方式
  • 数据处理层:负责具体的 SQL 查找、SQL 解析、SQL 执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

  • 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑

主要构件及其相互关系

构件 描述
SqlSession 作为 MyBatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护
StatementHandler 封装了 JDBC Statement 操作,负责对 JDBC Statement 的操作,如设置参数、将Statement 结果集转换成 List 集合
ParameterHandler 负责对用户传递的参数转换成 JDBC Statement 所需要的参数,
ResultSetHandler 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合;
TypeHandler 负责 Java 数据类型和 JDBC 数据类型之间的映射和转换
MappedStatement MappedStatement 维护了一条<select | update | delete | insert>节点的封装
SqlSource 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到BoundSql 对象中,并返回
BoundSql 表示动态生成的 SQL 语句以及相应的参数信息

img

总体流程

  1. 加载配置并初始化

    触发条件:加载配置文件

    配置来源于两个地方,一个是配置文件(主配置文件 sqlMapConfig.xml,Mapper 文件 *.xml),—个是 Java 代码中的注解,将主配置文件内容解析封装到 Configuration ,将 sql 的配置信息加载成为一个 MappedStatement 对象,存储在内存之中

  2. 接收调用请求

    触发条件:调用 MyBatis 提供的API

    传入参数:为 SQL 的 ID 和传入参数对象

    处理过程:将请求传递给下层的请求处理层进行处理。

  3. 处理操作请求

    触发条件:API 接口层传递请求过来

    传入参数:为 SQL 的 ID 和传入参数对象

    处理过程:

    1. 根据 SQL 的 ID 查找对应的 MappedStatement 对象。
    2. 根据传入参数对象解析 MappedStatement 对象,得到最终要执行的 SQL 和执行传入参数。
    3. 获取数据库连接,根据得到的最终 SQL 语句和执行传入参数到数据库执行,并得到执行结果。
    4. 根据 MappedStatement 对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果。
    5. 释放连接资源。
  4. 返回处理结果

    将最终的处理结果返回。

第十部分:MyBatis 源码剖析

第十一部分:设计模式

模式 MyBatis 体现
Builder 模式 例如 SqlSessionFactoryBuilder、Environment;
工厂方 法模式 例如 SqlSessionFactory、TransactionFactory、LogFactory
单例模 式 例如 ErrorContext 和 LogFactory;
代理模 式 MyBatis 实现的核心,比如 MapperProxy、ConnectionLogger,用的 JDK 的动态代理 还有executor.loader包使用了 cglib 或者 javassist 达到延迟加载的效果
组合模 式 例如 SqlNode 和各个子类 ChooseSqlNode 等;
模板方 法模式 例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的子类例如 IntegerTypeHandler;
适配器 模式 例如 Log 的 MyBatis 接口和它对 JDBC、Log4j 等各种日志框架的适配实现;
装饰者 模式 例如 Cache 包中的 cache.decorators 子包中等各个装饰者的实现;
迭代器 模式 例如迭代器模式 PropertyTokenizer;

参考资料

推荐阅读