MyBatis基础回顾及高级应用

377 阅读35分钟

Mybatis相关概念

对象/关系数据库映射(ORM)

ORM全称Object/Relation Mapping:表示对象-关系映射的缩写,ORM完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员既可以利用面向对象程序设计语言的简单易用性,又可以利用关系数据库的技术优势。ORM把关系数据库包装成面向对象的模型。ORM框架是面向对象设计语言与关系数据库发展不同步时的中间解决方案。采用ORM框架后,应用程序不再直接访问底层数据库,而是以面向对象的放松来操作持久化对象,而ORM框架则将这些面向对象的操作转换成底层SQL操作。ORM框架实现的效果:把对持久化对象的保存、修改、删除 等操作,转换为对数据库的操作

Mybatis简介

MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架【半自动指的是MyBatis还需要自己编写SQL语句】,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解来配置和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)为数据库中的记录。

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)

Mybatis优势

Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编码进行分离,功能边界清晰,一个专注业务,一个专注数据。

分析图示如下:

image.png

Mybatis基本应用

快速入门

MyBatis官网地址:www.mybatis.org/mybatis-3/

开发步骤:

  • ①添加MyBatis的坐标

  • ②创建user数据表

  • ③编写User实体类

  • ④编写映射文件UserMapper.xml

  • ⑤编写核心文件SqlMapConfig.xml

  • ⑥编写测试类

环境搭建

1)导入MyBatis的坐标和其他相关坐标

<?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.lagou</groupId>
    <artifactId>mybatis_quickStarter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!--引入依赖-->
    <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.6</version>
            <scope>runtime</scope>
        </dependency>
        <!--单元测试坐标-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

2) 创建user数据表

image.png

3) 编写User实体

public class User {

    private int id;

    private String username;

    private String password;

    //省略get个set方法

}

4)编写UserMapper映射文件

<?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.dao.IUserDao">
    <!--namespace : 名称空间:与id组成sql的唯一标识
        resultType: 表明返回值类型-->  
    <!--查询用户-->
    <select id="findAll" resultType="uSeR">
       select * from user
    </select>
</mapper>

5) 编写MyBatis核心文件

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

jdbc.properties文件如下:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///zdy_mybatis
jdbc.username=root
jdbc.password=root

6) 编写测试代码

public class MybatisTest {

    @Test
    public void test1() throws IOException {
        //1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        //2.解析了配置文件,并创建了sqlSessionFactory工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        //3.生产sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();// 默认开启一个事务,但是该事务不会自动提交
                                                                //在进行增删改操作时,要手动提交事务
        //4.sqlSession调用方法:查询所有selectList  查询单个:selectOne 添加:insert  修改:update 删除:delete
        //传入的就是statmentId,即mapper.xml中的namespace.id的值
        List<User> users = sqlSession.selectList("user.findAll");
        for (User user : users) {
            System.out.println(user);
        }
        sqlSession.close();
    }
}

MyBatis的CRUD操作

MyBatis的插入数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <insert id="add" parameterType="com.lagou.domain.User">
        insert into user values(#{id},#{username},#{password})
    </insert>
</mapper>

2)编写插入实体User的代码

InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//即namespace+id组成的statmentId
int insert = sqlSession.insert("userMapper.add", user);
System.out.println(insert);

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

3)插入操作注意问题

  • 插入语句使用insert标签

  • 在映射文件中使用parameterType属性指定要插入的数据类型

  • Sql语句中使用#{实体属性名}方式引用实体中的属性值

  • 插入操作使用的API是sqlSession.insert(“命名空间.id”,实体对象);

  • 插入操作涉及数据库数据变化,所以要使用sqlSession对象显示的提交事务,即sqlSession.commit()

MyBatis的修改数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <update id="update" parameterType="com.lagou.domain.User">
        update user set username=#{username},password=#{password} where id=#{id}
    </update>
</mapper>

2)编写修改实体User的代码

 
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

User user = new User();
user.setId(4);
user.setUsername("lucy");
//即namespace+id组成的statmentId
sqlSession.update("userMapper.updateUser",user);
sqlSession.commit();
sqlSession.close();

3)修改操作注意问题

  • 修改语句使用update标签

  • 修改操作使用的API是sqlSession.update(“命名空间.id”,实体对象);

MyBatis的删除数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <delete id="delete" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>
</mapper>

2)编写删除数据的代码

InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.delete("userMapper.deleteUser",6);
sqlSession.commit();
sqlSession.close();

3)删除操作注意问题

  • 删除语句使用delete标签

  • Sql语句中使用#{任意字符串}方式引用传递的单个参数

  • 删除操作使用的API是sqlSession.delete(“命名空间.id”,Object);

MyBatis的映射文件概述

image.png

入门核心配置文件分析

image.png

MyBatis常用配置解析

1)environments标签

数据库环境的配置,支持多环境配置

image.png

其中,事务管理器(transactionManager)类型有两种:

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

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

其中,数据源(dataSource)类型有三种:

  • UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。

  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。

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

2)mapper标签

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

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

Mybatis相应API介绍

SqlSession工厂构建器SqlSessionFactoryBuilder

常用API:SqlSessionFactory build(InputStream inputStream)

通过加载mybatis的核心文件的输入流的形式构建一个SqlSessionFactory对象

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

其中, Resources 工具类,这个类在 org.apache.ibatis.io 包中。Resources 类帮助你从类路径下、文件系统或一个 web URL 中加载资源文件。

SqlSession工厂对象SqlSessionFactory

SqlSessionFactory 有多个个方法创建SqlSession 实例。常用的有如下两个:

image.png

SqlSession会话对象

SqlSession 实例在 MyBatis 中是非常强大的一个类。在这里你会看到所有执行语句、提交或回滚事务和获取映射器实例的方法。

执行语句的方法主要有:

<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

操作事务的方法主要有:

void commit()
void rollback()

Mybatis的Dao层实现

传统开发方式

编写UserDao接口

public interface UserDao {
    List<User> findAll() throws IOException;
}

编写UserDaoImpl实现

public class UserDaoImpl implements UserDao {

    public List<User> findAll() throws IOException {

        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> userList = sqlSession.selectList("userMapper.findAll");
        sqlSession.close();
        return userList;
    }
}

测试传统方式

@Test
public void testTraditionDao() throws IOException {
    UserDao userDao = new UserDaoImpl();
    List<User> all = userDao.findAll();
    System.out.println(all);
}

代理开发方式

代理开发方式介绍

采用 Mybatis 的代理开发方式实现 DAO 层的开发,这种方式是我们后面进入企业的主流。Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。

Mapper 接口开发需要遵循以下规范:

  • Mapper.xml文件中的namespace与mapper接口的全限定名相同

  • Mapper接口方法名和Mapper.xml中定义的每个statement的id相同

  • Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同

  • Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

编写UserMapper接口

image.png

测试代理方式

@Test
public void testProxyDao() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //获得MyBatis框架生成的UserMapper接口的实现类
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.findById(1);
    System.out.println(user);
    sqlSession.close();
}

Mybatis配置文件深入

核心配置文件SqlMapConfig.xml

MyBatis核心配置文件层级关系

image.png

MyBatis常用配置解析

1)environments标签

参考上面

2)mapper标签

参考上面

3)Properties标签

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

image.png

4)typeAliases标签

类型别名是为Java 类型设置一个短的名字。原来的类型名称配置如下

image.png

配置typeAliases,为com.lagou.domain.User定义别名为user

image.png

但是如果一个项目有几百个实体类,这样就得配置一百多个标签,很麻烦,可以修改为包配置,即指定包下面的实体类使用类名作为别名不区分大小写

<!--给实体类的全限定类名给别名-->
<typeAliases>
    <!--给单独的实体起别名-->
  <!--  <typeAlias type="com.lagou.pojo.User" alias="user"></typeAlias>-->
    <!--批量起别名:该包下所有的类的本身的类名:别名还不区分大小写-->
    <package name="com.lagou.pojo"/>
</typeAliases>

上面我们是自定义的别名,mybatis框架已经为我们设置好的一些常用的类型的别名

image.png

映射配置文件mapper.xml

动态sql语句概述

Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动

态变化的,此时在前面的学习中我们的 SQL 就不能满足要求了。

参考的官方文档,描述如下:

image.png

动态SQL之if跟where

我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查

询,如果username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰

到。

编写Dao层接口如下:

//多条件组合查询:演示if
public List<User> findByCondition(User user); 

只用if判断时,需要额外加上where 1 = 1 恒等条件,动态SQL编写如下:

<!--抽取sql片段-->
<sql id="selectUser">
     select * from user
</sql>
<select id="findByCondition" parameterType="user" resultType="user">
    <include refid="selectUser"></include> where 1 = 1;
    <if test="id !=null">
        and id = #{id}
    </if>
    <if test="username !=null">
        and username = #{username}
    </if>
</select>

where标签的作用:会提供where关键字并且去掉第一个and关键字

所以上述的动态SQL可以改写如下:

<select id="findByCondition" parameterType="user" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <if test="id !=null">
            and id = #{id}
        </if>
        <if test="username !=null">
            and username = #{username}
        </if>
    </where>

</select>

当查询条件id和username都存在时,控制台打印的sql语句如下:

image.png

当查询条件只有id存在时,控制台打印的sql语句如下:

image.png

动态SQL之foreach

循环执行sql的拼接操作,例如:SELECT * FROM USER WHERE id IN (1,2,5)。

编写Dao层接口如下:

//多值查询:演示foreach
public List<User> findByIds(int[] ids);

xml如下:

<!--多值查询:演示foreach-->
<select id="findByIds" parameterType="list" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <foreach collection="array" open="id in (" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

foreach标签的属性含义如下:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}

  • open:代表语句的开始部分

  • close:代表结束部分

  • item:代表遍历集合的每个元素,生成的变量名

  • sperator:代表分隔符

测试代码片段如下:

image.png

SQL片段抽取之include

Sql 中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的。

<!--抽取sql片段-->
<sql id="selectUser">
     select * from user
</sql>
<!--多值查询:演示foreach-->
<select id="findByIds" parameterType="list" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <foreach collection="array" open="id in (" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

Mybatis复杂映射开发

一对一查询

一对一查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户。

一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户

image.png

一对一查询的语句

对应的sql语句:select * from orders o,user u where o.uid=u.id;

查询的结果如下:

image.png

创建Order和User实体

public class Order {

    private Integer id;
    private String orderTime;
    private Double total;
    
    // 表明该订单属于哪个用户
    private User user;
}
public class User {
    private int id;
    private String username;
    private String password;
    private Date birthday;
}

创建OrderMapper接口

public interface OrderMapper {
    List<Order> findAll();
}

配置OrderMapper.xml

<mapper namespace="com.lagou.mapper.IOrderMapper">
    <!--
        private Integer id;
    private String orderTime;
    private Double total;-->

    <resultMap id="orderMap" type="com.lagou.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.pojo.User">
            <result property="id" column="uid"></result>
            <result property="username" column="username"></result>
        </association>
    </resultMap>

    <!--resultMap:手动来配置实体属性与表字段的映射关系-->
    <select id="findOrderAndUser" resultMap="orderMap">
        select * from orders o,user u where o.uid = u.id
    </select>
</mapper>

本质就是将SQL查询出来的列名与实体类的属性进行映射。

image.png

测试结果

image.png

image.png

一对多查询

一对多查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单。

image.png

一对多查询的语句

对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;

查询的结果如下:

image.png

修改User实体

public class Order {

    private int id;
    private Date ordertime;
    private double total;

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

public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前用户具备哪些订单
    private List<Order> orderList;
}

创建UserMapper接口

public interface UserMapper {
    List<User> findAll();
}

配置UserMapper.xml

<mapper namespace="com.lagou.mapper.UserMapper">
    <resultMap id="userMap" type="com.lagou.domain.User">
        <result column="id" property="id"></result>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="birthday" property="birthday"></result>
        <collection property="orderList" ofType="com.lagou.domain.Order">
            <result column="oid" property="id"></result>
            <result column="ordertime" property="ordertime"></result>
            <result column="total" property="total"></result>
        </collection>
    </resultMap>

    <select id="findAll" resultMap="userMap">
        select *,o.id oid from user u left join orders o on u.id=o.uid
    </select>
</mapper>

测试结果

image.png

多对多查询

多对多查询的模型

用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用

多对多查询的需求:查询用户同时查询出该用户的所有角色

image.png

多对多查询的语句

对应的sql语句:select u.,r.,r.id rid from user u left join user_role ur on u.id=ur.user_id inner join role r on ur.role_id=r.id;

查询的结果如下

image.png

创建Role实体,修改User实体

public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;

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

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

添加UserMapper接口方法

List<User> findAllUserAndRole();

配置UserMapper.xml

<resultMap id="userRoleMap" type="com.lagou.pojo.User">
    <result property="id" column="userid"></result>
    <result property="username" column="username"></result>
    <collection property="roleList" ofType="com.lagou.pojo.Role">
        <result property="id" column="roleid"></result>
        <result property="roleName" column="roleName"></result>
        <result property="roleDesc" column="roleDesc"></result>
    </collection>
</resultMap>


<select id="findAllUserAndRole" resultMap="userRoleMap">
    select * from user u left join sys_user_role ur on u.id = ur.userid
                   left join sys_role r on r.id = ur.roleid
</select>

测试结果

image.png

Mybatis注解开发

MyBatis的常用注解

这几年来注解开发越来越流行,Mybatis也可以使用注解开发方式,这样我们就可以减少编写Mapper

映射文件了。我们先围绕一些基本的CRUD来学习,再学习复杂映射多表操作。

  • @Insert:实现新增

  • @Update:实现更新

  • @Delete:实现删除

  • @Select:实现查询

  • @Result:实现结果集封装

  • @Results:可以与@Result 一起使用,封装多个结果集

  • @One:实现一对一结果集封装

  • @Many:实现一对多结果集封装

MyBatis的增删改查

dao接口中的代码如下:

//添加用户
@Insert("insert into user values(#{id},#{username})")
public void addUser(User user);

//更新用户
@Update("update user set username = #{username} where id = #{id}")
public void updateUser(User user);

//查询用户
@Select("select * from user")
public List<User> selectUser();

//删除用户
@Delete("delete from user where id = #{id}")
public void deleteUser(Integer id);

注意,在SqlMapConfig.xml指定的应该是mapper接口的路径,而不再是mapper.xml的路径,所以必须使用如下配置:

<!--引入映射配置文件-->
<mappers>
   <!-- <mapper class="com.lagou.mapper.IUserMapper"></mapper>-->
    <package name="com.lagou.mapper"/>
</mappers>

使用class指定的话,每一个mapper都得写一个很麻烦,直接使用package扫描包很方便。注意此时mapper.xml应该不能与mapper的路径一样,否则会出现重复的错误。

MyBatis的注解实现复杂映射开发

实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用

@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置。

image.png

image.png

一对一查询

一对一查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户.

image.png

一对一查询的语句

对应的sql语句:

select * from orders;

select * from user where id=查询出订单的uid;

查询的结果如下:

image.png

创建Order和User实体

public class Order {

    private int id;

    private Date ordertime;

    private double total;

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

public class User {

    private int id;

    private String username;

    private String password;

    private Date birthday;

}

创建OrderMapper接口

@Results({
        @Result(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.mapper.IUserMapper.findUserById"))
})
@Select("select * from orders")
public List<Order> findOrderAndUser();

//根据id查询用户
@Select({"select * from user where id = #{id}"})
public User findUserById(Integer id);

查询分析图如下

image.png

测试结果

@Test
public void testSelectOrderAndUser() {
    List<Order> all = orderMapper.findAll();
    for(Order order : all){
        System.out.println(order);
    }
}

image.png

一对多查询

一对多查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单

image.png

一对多查询的语句

对应的sql语句:

select * from user;

select * from orders where uid=查询出用户的id;

查询的结果如下:

image.png

修改User实体

public class Order {

    private int id;

    private Date ordertime;

    private double total;

    //代表当前订单从属于哪一个客户

    private User user;

}

public class User {

    private int id;

    private String username;

    private String password;

    private Date birthday;

    //代表当前用户具备哪些订单

    private List<Order> orderList;

创建UserMapper接口

//查询所有用户、同时查询每个用户关联的订单信息
@Select("select * from user")
@Results({
        @Result(property = "id",column = "id"),
        @Result(property = "username",column = "username"),
        @Result(property = "orderList",column = "id",javaType = List.class,
            many=@Many(select = "com.lagou.mapper.IOrderMapper.findOrderByUid"))
})
public List<User> findAll();
@Select("select * from orders where uid = #{uid}")
public List<Order> findOrderByUid(Integer uid);

测试结果

image.png

多对多查询

多对多查询的模型

用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用

多对多查询的需求:查询用户同时查询出该用户的所有角色。

image.png

多对多查询的语句

对应的sql语句:

select * from user;

select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=用户的id

image.png

创建Role实体,修改User实体

public class User {

    private int id;

    private String username;

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

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

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

添加UserMapper接口方法

//查询所有用户、同时查询每个用户关联的角色信息
@Select("select * from user")
@Results({
        @Result(property = "id",column = "id"),
        @Result(property = "username",column = "username"),
        @Result(property = "roleList",column = "id",javaType = List.class,
         many = @Many(select = "com.lagou.mapper.IRoleMapper.findRoleByUid"))
})
public List<User> findAllUserAndRole();
@Select("select * from sys_role r,sys_user_role ur where r.id = ur.roleid and ur.userid = #{uid}")
public List<Role> findRoleByUid(Integer uid);

测试结果

image.png

Mybatis缓存

一级缓存

①、在一个sqlSession中,对User表根据id进行两次查询,查看他们发出sql语句的情况

@Test

public void test1(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
    User u1 = userMapper.selectUserByUserId(1);
    System.out.println(u1);

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

查看控制台打印情况:

image.png

② 、同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。

@Test

public void test2(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    //第一次查询,发出sql语句,并将查询的结果放入缓存中
    User u1 = userMapper.selectUserByUserId( 1 );
    System.out.println(u1);

    //第二步进行了一次更新操作,sqlSession.commit()
    u1.setSex("女");
    userMapper.updateUserByUserId(u1);
    sqlSession.commit();

    //第二次查询,由于是同一个sqlSession.commit(),会清空缓存信息
    //则此次查询也会发出sql语句
    User u2 = userMapper.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession.close();
}

查看控制台打印情况:

image.png

③、总结

  • 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。

  • 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

  • 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。

  • 手动调用sqlSession.clearCache()同样也会清空一级缓存。

image.png

暂时理解SqlSession缓存中的HashMao结构中的Key由如下组成:由statmentId(即namespace.id),param(即sql的占位符参数值),BoundSql,rowBounds(分页对象)

value是查询的结果

一级缓存原理探究与源码分析

一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?相信你现在应该会有这几个疑问,那么我们本节就来研究一下一级缓存的本质

大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开SqlSession,所以索性我们 就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法。

image.png

调研了一圈,发现上述所有方法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入手吧,分析源码时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,你才会对这个类有更深的认识,分析了一圈,你可能会得到如下这个流程图。

image.png

image.png

image.png

image.png

image.png

image.png

再深入分析,流程走到Perpetualcache中的clear()方法之后,会调用其cache.clear()方法,那么这个cache是什么东西呢?点进去发现,cache其实就是private Map cache = new HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是本地存放的一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何时创建的呢

你觉得最有可能创建缓存的地方是哪里呢?我觉得是Executor,为什么这么认为?因为Executor是执行器,用来执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很 有可能在Executor中,看了一圈发现Executor中有一个createCacheKey方法,这个方法很像是创建缓存的方法啊,跟进去看看,你发现createCacheKey方法是由BaseExecutor执行的,代码如下:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  //MappedStatement的id就是statmentId
  cacheKey.update(ms.getId());
  //分页的偏移量
  cacheKey.update(rowBounds.getOffset());
  //每页条数
  cacheKey.update(rowBounds.getLimit());
  //真正执行的SQL语句
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    //SqlMapConfig.xml中的<environment>标签中的id值,即指定数据源
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

接着看一下CacheKey中的update方法如下:

image.png

image.png

创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个update方法最终由updateList的list来把五个值存进去,对照上面的代码和下面的图示,你应该能 理解这五个值都是什么了

image.png 这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在SqlMapConfig.xml中的标签,见如下。

<environments default="development">
    //就是id值,即development
    <environment id="development">
    <transactionManager type="JDBC"/>
    <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>

那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到BaseExecutor中的query方法如下:

image.png

image.png

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。 localcache对象的put方法最终交给Map进行存放

image.png

二级缓存

二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。

image.png

注意:MyBatis的一级缓存是默认开启的,但是二级缓存不是,需要手动配置

①、基于注解的方式简单演示如下:

1.首先在SqlMapConfig.xml配置文件中开启二级缓存(其实默认该配置也是true)

<!--开启二级缓存-->
<settings>
〈setting name="cacheEnabled" value="true"/>
</settings>

2.接着在IUserMapper接口中加入注解@CacheNamespace,代码如下:

@CacheNamespace//开启二级缓存
public interface IUserMapper {
 
    //根据id查询用户
    @Select({"select * from user where id = #{id}"})
    public User findUserById(Integer id);
}

3.将实体类必须实现序列化接口Serializable,如下;

public class User implements Serializable {
  //属性省略

这是为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。

4.编写测试类如下:

/**
 * 注意:二级缓存缓存的是对象中的数据,而不是对象
 */
@Test
public void SecondLevelCache(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
     
    IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
    IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
    
    User user1 = mapper1.findUserById(1);
    sqlSession1.close(); //清空一级缓存

    User user2 = mapper2.findUserById(1);
    System.out.println(user1==user2);
}

演示发现第第二次查询时,没有打印SQL,并且日志显示缓存命中,说明第一次查询把数据库中数据存入到二级缓存中,第二查询直接从缓存中取数据。但是上述代码中打印的user对象的地址不一样,这是为什么?

因为二级缓存中缓存的是对象的数据,而不是对象本身,即从二级缓存中取数据时,会将之前缓存的数据另外封装成一个新的对象返回。

如果在两个查询中间进行增删改,移交事务操作的话同样也会清空二级缓存,,代码演示如下:

/**
 * 注意:二级缓存缓存的是对象中的数据,而不是对象
 */
@Test
public void SecondLevelCache(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
    IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
    IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);

    User user1 = mapper1.findUserById(1);
    sqlSession1.close(); //清空一级缓存


    User user = new User();
    user.setId(1);
    user.setUsername("lisi");
    mapper3.updateUser(user);
    sqlSession3.commit();

    User user2 = mapper2.findUserById(1);
    System.out.println(user1==user2);
}

上述代码中两次查询都会查询数据库,因为中间增加了更新操作,会清空二级缓存

image.png

useCache和flushCache

当然也可以指定每一个查询方法不使用二级缓存(即该查询禁用二级缓存),可以通过@Options(useCache = false)来设置。代码如下:

/**
 * implementation属性中指明MyBatis二级缓存的实现类是什么
 * MyBatis自己的二级缓存默认的实现类是PerpetualCache类。
 */
@CacheNamespace(implementation = RedisCache.class)//开启二级缓存
public interface IUserMapper {

    //该查询禁用二级缓存
    @Options(useCache = false)
    @Select({"select * from user where id = #{id}"})
    public User findUserById(Integer id);
}

在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可。

②、基于xml的方式简单演示如下:

1.首先在SqlMapConfig.xml配置文件中开启二级缓存(其实默认该配置也是true),这里跟注解方式一样。

<!--开启二级缓存-->
<settings>
〈setting name="cacheEnabled" value="true"/>
</settings>>

2.其次在UserMapper.xml文件中开启缓存

<!--开启二级缓存-->
<cache></cache>

我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是mybatis默认实现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口来自定义缓存。

image.png

public class PerpetualCache implements Cache {
    private final String id;
    private MapcObject, Object> cache = new HashMapC);
    public PerpetualCache(St ring id) { this.id = id;
}

我们可以看到二级缓存底层还是HashMap结构。

二级缓存整合redis

上面我们介绍了 mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了1服务器,查询后的缓存就会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个缓存,如下图所示:

image.png

为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:

image.png

如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中, 然后无论有多少台服务器,我们都能从缓存中获取数据。

这里我们介绍mybatis与redis的整合。刚刚提到过,mybatis提供了一个eache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。

mybatis本身默认实现了一个,即PerpetualCache,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。redis分布式缓存就可以,mybatis提供了一个针对cache接口的redis实现类,该类存在mybatis-redis包中实现。

整合步骤1:引入MyBAatis与Redis整合的依赖,如下

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

整合步骤2:在Mapper.xml指定MyBatis二级缓存的实现类

<?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.mapper.IUserMapper">
    <cache type="org.mybatis.caches.redis.RedisCache" />

    <select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
        select * from user
    </select>

或者使用注解方式指明,在IUserMapper接口中指明如下

/**
 * implementation属性中指明MyBatis二级缓存的实现类是什么
 * MyBatis自己的二级缓存默认的实现类是PerpetualCache类。
 */
@CacheNamespace(implementation = RedisCache.class)//开启二级缓存
public interface IUserMapper {

    //根据id查询用户
    @Options(useCache = true)
    @Select({"select * from user where id = #{id}"})
    public User findUserById(Integer id);
}

整合步骤3:在resource目录下创建redis.properties文件(为什么一定要叫这个文件名,在后续分析RedisCache源码会讲到

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

整合步骤4:测试

/**
 * 注意:二级缓存缓存的是对象中的数据,而不是对象
 */
@Test
public void SecondLevelCache(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
    IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
    IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);

    User user1 = mapper1.findUserById(1);
    sqlSession1.close(); //清空一级缓存

    User user = new User();
    user.setId(1);
    user.setUsername("lisi");
    mapper3.updateUser(user);
    sqlSession3.commit();

    User user2 = mapper2.findUserById(1);
    System.out.println(user1==user2);
}

RedisCache源码分析

RedisCache和大家普遍实现Mybatis的缓存PerpetualCache方案大同小异,无非是实现Cache接口,并使用jedis操作缓存;不过该项目在设计细节上有一些区别;

public RedisCache(String id) {
    if (id == null) {
        throw new IllegalArgumentException("Cache instances require an ID");
    } else {
        this.id = id;
        RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
        pool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
    }
}

RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中,调用了 RedisConfigurationBuilder 来创建RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。RedisConfig类继承JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的属性:

image.png

RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:

image.png

image.png

public RedisConfig parseConfiguration(ClassLoader classLoader) {
   Properties config = new Properties();
   
   //这里加载就是resource目录下的redis.properties文件
   InputStream input = classLoader.getResourceAsStream(redisPropertiesFilename);
   if (input != null) {
      try {
         config.load(input);
      } catch (IOException e) {
         throw new RuntimeException(
               "An error occurred while reading classpath property '"
                     + redisPropertiesFilename
                     + "', see nested exceptions", e);
      } finally {
         try {
            input.close();
         } catch (IOException e) {
            // close quietly
         }
      }
   }
   
   //创建RedisConfig
   RedisConfig jedisConfig = new RedisConfig();
   setConfigProperties(config, jedisConfig);
   return jedisConfig;
}

查看redisPropertiesFilename属性发现就是指定加载resource目录下的redis.properties文件

image.png

接下来,就是RedisCache使用RedisConfig类创建完成edisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:

private Object execute(RedisCallback callback) {
  Jedis jedis = pool.getResource();
  try {
    return callback.doWithRedis(jedis);
  } finally {
    jedis.close();
  }
}

接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis储存数据的格式为Hash类型

@Override
public void putObject(final Object key, final Object value) {
  execute(new RedisCallback() {
    @Override
    public Object doWithRedis(Jedis jedis) {
      jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
      return null;
    }
  });
}

@Override
public Object getObject(final Object key) {
  return execute(new RedisCallback() {
    @Override
    public Object doWithRedis(Jedis jedis) {
      return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));
    }
  });
}

可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;

Mybatis插件

插件简介

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBati s插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。

Mybatis插件介绍

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

image.png

MyBatis所允许拦截的方法如下

  • 执行器Executor (update、query、commit、rollback等方法);

  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);

  • 参数处理器ParameterHandler (getParameterObject、setParameters方法);

  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);

Mybatis插件原理

在四大对象创建的时候

  • 1、每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);

  • 2、获取到所有的Interceptor (拦截器)(插件需要实现的接口);调用 interceptor.plugin(target);返回 target 包装后的对象

  • 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行;

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说

public ParameterHandler newParameterHandler(MappedStatement mappedStatement,Object object, BoundSql sql, InterceptorChain interceptorChain){

    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);

    parameterHandler = (ParameterHandler)interceptorChain.pluginAll(parameterHandler);

    return parameterHandler;

}

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。

interceptor.plugin(target)中的target就可以理解为mybatis中的四大对象。返回的target是被重重代理后的对象,如果我们想要拦截Executor的query方法,那么可以这样定义插件:

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
    )
})

public class ExeunplePlugin implements Interceptor {
    //省略逻辑
}

除此之外,我们还需将插件配置到sqlMapConfig.xm l中。

<plugins>
    <plugin interceptor="com.lagou.plugin.ExamplePlugin">
    </plugin>
</plugins>

这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创建SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执行。

以上就是MyBatis插件机制的基本原理

自定义插件

Mybatis 插件接口-Interceptor

  • Intercept方法,插件的核心方法

  • plugin方法,生成target的代理对象

  • setProperties方法,传递插件所需参数

@Intercepts({
        @Signature(type= StatementHandler.class,//指定对哪个类
                  method = "prepare",//指明对哪个方法进行拦截
                  args = {Connection.class,Integer.class})//方法的参数,因为可能存在方法重载
})
public class MyPlugin implements Interceptor {

    /*
        拦截方法:只要被拦截的目标对象的目标方法被执行时,每次都会执行intercept方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("对方法进行了增强....");
        return invocation.proceed(); //原方法执行
    }

    /*
       主要为了把当前的拦截器生成代理存到拦截器链中
     */
    @Override
    public Object plugin(Object target) {
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }

    /*
        获取配置文件的参数
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("获取到的配置文件的参数是:"+properties);
    }
}

在sqlMapConfig.xml配置插件如下:

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

插件源码分析

Plugin实现了 InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会 对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throwsThrowable {

    try {
    /*
     *获取被拦截方法列表,比如:
     * signatureMap.get(Executor.class), 可能返回 [query, update, commit]
     */

    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    //检测方法列表是否包含被拦截的方法
    if (methods != null && methods.contains(method)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
    } catch(Exception e){
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截方法是否配置在插件的@Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept中,该方法的参数类型为Invocationo Invocation主要用于存储目标类,方法以及方法参数列表。下面简单看一下该类的定义

public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

关于插件的执行逻辑就分析结束

pageHelper分页插件

MyBati s可以使用第三方的插件来对功能进行扩展,分页助手PageHelper是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据

开发步骤:

  • ① 导入通用PageHelper的坐标

  • ② 在mybatis核心配置文件中配置PageHelper插件

  • ③ 测试分页数据获取

①导入通用PageHelper坐标

<!-- 分页助手 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>3.7.5</version>
</dependency>
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>0.9.1</version>
</dependency>

② 在mybatis核心配置文件中配置PageHelper插件

<!--注意:分页助手的插件 配置在通用馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper">
    <!—指定方言 —>
    <property name="dialect" value="mysql"/>
</plugin>

③ 测试分页代码实现

@Test
public void pageHelperTest(){

    PageHelper.startPage(1,1);
    List<User> users = userMapper.selectUser();
    for (User user : users) {
        System.out.println(user);
    }

    PageInfo<User> pageInfo = new PageInfo<>(users);
    System.out.println("总条数:"+pageInfo.getTotal());
    System.out.println("总页数:"+pageInfo.getPages());
    System.out.println("当前页:"+pageInfo.getPageNum());
    System.out.println("每页显示的条数:"+pageInfo.getPageSize());
}

数据权限插件

/**
 * @Auther: huangshuai
 * @Date: 2020/11/23 23:29
 * @Description:给sql后面添加where查询条件
 * @Version:
 */

/**
 * 开发自己的插件
 * OneInterceptor相当于代理实现类
 * typ表示对哪个神器进行拦截
 * method:神器的具体方法
 * args:方法的参数类型
 */
@Intercepts(value = @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
public class OneInterceptor implements Interceptor {


    private final String key = "factory_code";
    private final List<String> value = Arrays.asList("111", "222");

    /**
     * 该方法的作用:【任务】为当前目标对象生成代理对象
     * <p>
     * 监听对象(代理对象)=Proxy.newProxyInstance(类加载器,interfaces,代理实现里)
     * 上面的代码可以用Plugin工具类来完成,因为该工具类中的wrap方法封装了上一行代码
     * <p>
     * target就是四大神器的实例对象
     * 返回值就是四大神器的监听对象即代理对象
     */
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 相当于InvocationHandler中的Invoke方法
     * invoke(Object proxy,Method method,Object[] args);
     * <p>
     * 该方法中的参数Invocation invocation,封装的是被拦截的神器对象,还封装了被拦截神器的行为,即神器的方法
     * 例如封装了StatementHandler对象,封装了该对象中的prepare方法
     * <p>
     * 该方法的返回值就是神器的被拦截的方法的返回值
     */
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            metaObject.setValue("delegate.boundSql.sql", rewriteSql(metaObject));
        }
        return invocation.proceed();

    }

    private String rewriteSql(MetaObject metaObject) throws Exception {
        String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
        CCJSqlParserManager sqlParserManager = new CCJSqlParserManager();
        Select select = (Select) sqlParserManager.parse(new StringReader(originalSql));
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            this.setWhere((PlainSelect) selectBody, key, value);
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = operationList.getSelects();
            selectBodyList.forEach(s -> {
                try {
                    this.setWhere((PlainSelect) s, key, value);
                } catch (Exception e) {

                }

            });
        }
        return select.toString();
    }

    private void setWhere(PlainSelect plainSelect, String key, List<String> value) throws Exception {
        Table fromItem = (Table) plainSelect.getFromItem();
        Alias alias = fromItem.getAlias();
        String mainTableName = alias == null ? fromItem.getName() : alias.getName();
        ExpressionList expressionList = new ExpressionList(value.stream().map(StringValue::new).collect(Collectors.toList()));
        InExpression inExpression = new InExpression(new Column(mainTableName + "." + key), expressionList);
        if (plainSelect.getWhere() == null) {
            plainSelect.setWhere(CCJSqlParserUtil.parseCondExpression(inExpression.toString()));
        } else {
            plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), CCJSqlParserUtil.parseExpression(inExpression.toString())));
        }


    }

    /**
     * 该方法的任务:为拦截器对象的属性进行赋值的
     *
     * <plugins>
     * <plugin interceptor="com.kaikeba.interceptor.OneInterceptor">
     * <!-- 该属性的值,可以在拦截器中的setProperties方法中进行读取 -->
     * <property name="driver" value="com.mysql.jdbc.Driver"/>
     * </plugin>
     * </plugins>
     */
    public void setProperties(Properties properties) {
        String property = properties.getProperty("driver");
        System.out.println("获得属性值是:" + property);
    }

}    

通用 mapper

什么是通用Mapper

通用Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。

如何使用通用Mappe?

  1. 首先在maven项目,在pom.xml中引入mapper的依赖
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>3.1.2</version>
</dependency>
  1. Mybatis配置文件中完成配置
<plugins>
<!--       <plugin interceptor="com.lagou.plugin.MyPlugin">
           <property name="name" value="tom"/>
       </plugin>-->
        <!--分页插件:如果有分页插件,要排在通用mapper之前-->
       <plugin interceptor="com.github.pagehelper.PageHelper">
           <property name="dialect" value="mysql"/>
       </plugin>
       
       <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
           <!--指定当前通用mapper接口使用的是哪一个-->
           <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
       </plugin>
   </plugins>
  1. 实体类设置主键
@Table(name = "t_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)   
    private Integer id;
    
    private String username;
}
  1. 定义通用mapper
public interface UserMapper  extends Mapper<User> {
}
  1. 测试
@Test
public void mapperTest() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = new User();
    user.setId(1);
    User user1 = mapper.selectOne(user);
    System.out.println(user1);

    //2.example方法
    Example example = new Example(User.class);
    example.createCriteria().andEqualTo("id",1);
    List<User> users = mapper.selectByExample(example);
    for (User user2 : users) {
        System.out.println(user2);
    }
}