MyBatis初级

目标

  1. 优点
  2. # 和 $
  3. 插件机制
  4. 缓存机制
  5. 连接池
  6. 动态SQL
  7. 分页
  8. MyBatisPlus

基本介绍

ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象

MyBatis

  • MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。

  • MyBatis 通过 XML 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。

  • MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC 底层 API 的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。

#{}和${}

#{}:占位符,传入的内容会作为字符串加上引号,以预编译的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值,有效的防止 SQL 注入,提高系统安全性

${}:拼接符,传入的内容会直接替换拼接,不会加上引号,可能存在 sql 注入的安全隐患

  • 能用 #{} 的地方就用 #{},不用或少用 ${}

  • 必须使用 ${} 的情况:

    • 表名作参数时,如:SELECT * FROM ${tableName}
    • order by 时,如:SELECT * FROM t_user ORDER BY ${columnName}
  • sql 语句使用 #{},properties 文件内容获取使用 ${}

缓存机制

缓存概述

缓存:缓存就是一块内存空间,保存临时数据

作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性能

缓存适用:

  • 适用于缓存的:经常查询但不经常修改的,数据的正确与否对最终结果影响不大的
  • 不适用缓存的:经常改变的数据 , 敏感数据(例如:股市的牌价,银行的汇率,银行卡里面的钱)等等

缓存类别:

  • 一级缓存:SqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 SqlSession 一致。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据,不同的 SqlSession 之间的缓存数据区域是互相不影响的
  • 二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 SQL 可以共用二级缓存,二级缓存是跨 SqlSession 的

开启缓存:配置核心配置文件中 标签

  • cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存,默认 true

参考文章:https://www.cnblogs.com/ysocean/p/7342498.html


一级缓存

一级缓存是 SqlSession 级别的缓存

工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。

一级缓存的失效:

  • SqlSession 不同
  • SqlSession 相同,查询条件不同时(还未缓存该数据)
  • SqlSession 相同,手动清除了一级缓存,调用 sqlSession.clearCache()
  • SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读

Spring 整合 MyBatis 后,一级缓存作用:

  • 未开启事务的情况,每次查询 Spring 都会创建新的 SqlSession,因此一级缓存失效
  • 开启事务的情况,Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession,因此此时一级缓存是有效的

测试一级缓存存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void testFirstLevelCache(){
//1. 获取sqlSession对象
SqlSession sqlSession = SqlSessionFactoryUtils.openSession();
//2. 通过sqlSession对象获取UserDao接口的代理对象
UserDao userDao1 = sqlSession.getMapper(UserDao.class);
//3. 调用UserDao接口的代理对象的findById方法获取信息
User user1 = userDao1.findById(1);
System.out.println(user1);

//sqlSession.clearCache() 清空缓存

UserDao userDao2 = sqlSession.getMapper(UserDao.class);
User user = userDao.findById(1);
System.out.println(user2);

//4.测试两次结果是否一样
System.out.println(user1 == user2);//true

//5. 提交事务关闭资源
SqlSessionFactoryUtils.commitAndClose(sqlSession);
}

二级缓存

基本介绍

二级缓存是 mapper 的缓存,只要是同一个命名空间(namespace)的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存

作用:作用范围是整个应用,可以跨线程使用,适合缓存一些修改较少的数据

工作流程:一个会话查询数据,这个数据就会被放在当前会话的一级缓存中,如果会话关闭或提交一级缓存中的数据会保存到二级缓存

二级缓存的基本使用:

  1. 在 MyBatisConfig.xml 文件开启二级缓存,cacheEnabled 默认值为 true,所以这一步可以省略不配置

    1
    2
    3
    4
    <!--配置开启二级缓存-->
    <settings>
    <setting name="cacheEnabled" value="true"/>
    </settings>
  2. 配置 Mapper 映射文件

    <cache> 标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace 值

    1
    2
    3
    4
    5
    <mapper namespace="dao.UserDao">
    <!--开启user支持二级缓存-->
    <cache eviction="FIFO" flushInterval="6000" readOnly="" size="1024"/>
    <cache></cache> <!--则表示所有属性使用默认值-->
    </mapper>

    eviction(清除策略):

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

    flushInterval(刷新间隔):可以设置为任意的正整数, 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新

    size(引用数目):缓存存放多少元素,默认值是 1024

    readOnly(只读):可以被设置为 true 或 false

    • 只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,促进了性能提升
    • 可读写的缓存会(通过序列化)返回缓存对象的拷贝, 速度上会慢一些,但是更安全,因此默认值是 false

    type:指定自定义缓存的全类名,实现 Cache 接口即可

  3. 要进行二级缓存的类必须实现 java.io.Serializable 接口,可以使用序列化方式来保存对象。

    1
    public class User implements Serializable{}

相关属性

  1. select 标签的 useCache 属性

    映射文件中的 <select> 标签中设置 useCache="true" 代表当前 statement 要使用二级缓存(默认)

    注意:如果每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存

    1
    2
    3
    <select id="findAll" resultType="user" useCache="true">
    select * from user
    </select>
  2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在执行增删改之后就会清除一、二级缓存,保证缓存的一致性;而查询标签默认值为 false,所以查询不会清空缓存

  3. localCacheScope:本地缓存作用域, 中的配置项,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存


源码解析

事务提交二级缓存才生效:DefaultSqlSession 调用 commit() 时会回调 executor.commit()

  • CachingExecutor#query():执行查询方法,查询出的数据会先放入 entriesToAddOnCommit 集合暂存

    1
    2
    3
    4
    5
    6
    7
    8
    // 从二缓存中获取数据,获取不到去一级缓存获取
    List<E> list = (List<E>) tcm.getObject(cache, key);
    if (list == null) {
    // 回调 BaseExecutor#query
    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    // 将数据放入 entriesToAddOnCommit 集合暂存,此时还没放入二级缓存
    tcm.putObject(cache, key, list);
    }
  • commit():事务提交,清空一级缓存,放入二级缓存,二级缓存使用 TransactionalCacheManager(tcm)管理

    1
    2
    3
    4
    5
    public void commit(boolean required) throws SQLException {
    // 首先调用 BaseExecutor#commit 方法,【清空一级缓存】
    delegate.commit(required);
    tcm.commit();
    }
  • TransactionalCacheManager#commit:查询出的数据放入二级缓存

    1
    2
    3
    4
    5
    6
    public void commit() {
    // 获取所有的缓存事务,挨着进行提交
    for (TransactionalCache txCache : transactionalCaches.values()) {
    txCache.commit();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void commit() {
    if (clearOnCommit) {
    delegate.clear();
    }
    // 将 entriesToAddOnCommit 中的数据放入二级缓存
    flushPendingEntries();
    // 清空相关集合
    reset();
    }
    1
    2
    3
    4
    5
    6
    private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
    // 将数据放入二级缓存
    delegate.putObject(entry.getKey(), entry.getValue());
    }
    }

增删改操作会清空缓存:

  • update():CachingExecutor 的更新操作

    1
    2
    3
    4
    5
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    // 回调 BaseExecutor#update 方法,也会清空一级缓存
    return delegate.update(ms, parameterObject);
    }
  • flushCacheIfRequired():判断是否需要清空二级缓存

    1
    2
    3
    4
    5
    6
    7
    8
    private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // 判断二级缓存是否存在,然后判断标签的 flushCache 的值,增删改操作的 flushCache 属性默认为 true
    if (cache != null && ms.isFlushCacheRequired()) {
    // 清空二级缓存
    tcm.clear(cache);
    }
    }

自定义缓存

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

type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器

1
2
3
4
5
6
7
8
9
public interface Cache {
String getId();
int getSize();
void putObject(Object key, Object value);
Object getObject(Object key);
boolean hasKey(Object key);
Object removeObject(Object key);
void clear();
}

缓存的配置,只需要在缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如在缓存实现上调用一个名为 setCacheFile(String file) 的方法:

1
2
3
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
  • 可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。
  • 可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值

MyBatis 支持在所有属性设置完毕之后,调用一个初始化方法, 如果想要使用这个特性,可以在自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口

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

注意:对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存

对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新,在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存

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

构造语句

动态 SQL

基本介绍

动态 SQL 是 MyBatis 强大特性之一,逻辑复杂时,MyBatis 映射配置文件中,SQL 是动态变化的,所以引入动态 SQL 简化拼装 SQL 的操作

DynamicSQL 包含的标签:

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

各个标签都可以进行灵活嵌套和组合

OGNL:Object Graphic Navigation Language(对象图导航语言),用于对数据进行访问

参考文章:https://www.cnblogs.com/ysocean/p/7289529.html


where

:条件标签,有动态条件则使用该标签代替 WHERE 关键字,封装查询条件

作用:如果标签返回的内容是以 AND 或 OR 开头的,标签内会剔除掉

表结构:


if

基本格式:

1
2
3
<if test=“条件判断”>
查询条件拼接
</if>

我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果username 不同空时还要加入用户名作为条件,这种情况在我们的多条件组合查询中经常会碰到。

  • UserMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?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="mapper.UserMapper">
    <select id="selectCondition" resultType="user" parameterType="user">
    SELECT * FROM user
    <where>
    <if test="id != null ">
    id = #{id}
    </if>
    <if test="username != null ">
    AND username = #{username}
    </if>
    <if test="sex != null ">
    AND sex = #{sex}
    </if>
    </where>
    </select>

    </mapper>
  • MyBatisConfig.xml,引入映射配置文件

    1
    2
    3
    4
    <mappers>
    <!--mapper引入指定的映射配置 resource属性执行的映射配置文件的名称-->
    <mapper resource="UserMapper.xml"/>
    </mappers>
  • DAO 层 Mapper 接口

    1
    2
    3
    4
    public interface UserMapper {
    //多条件查询
    public abstract List<User> selectCondition(Student stu);
    }
  • 实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class DynamicTest {
    @Test
    public void selectCondition() throws Exception{
    //1.加载核心配置文件
    InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

    //2.获取SqlSession工厂对象
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);

    //3.通过工厂对象获取SqlSession对象
    SqlSession sqlSession = ssf.openSession(true);

    //4.获取StudentMapper接口的实现类对象
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = new User();
    user.setId(2);
    user.setUsername("李四");
    //user.setSex(男); AND 后会自动剔除

    //5.调用实现类的方法,接收结果
    List<Student> list = mapper.selectCondition(user);

    //6.处理结果
    for (User user : list) {
    System.out.println(user);
    }

    //7.释放资源
    sqlSession.close();
    is.close();
    }
    }

set

:进行更新操作的时候,含有 set 关键词,使用该标签

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 根据 id 更新 user 表的数据 -->
<update id="updateUserById" parameterType="com.ys.po.User">
UPDATE user u
<set>
<if test="username != null and username != ''">
u.username = #{username},
</if>
<if test="sex != null and sex != ''">
u.sex = #{sex}
</if>
</set>
WHERE id=#{id}
</update>
  • 如果第一个条件 username 为空,那么 sql 语句为:update user u set u.sex=? where id=?
  • 如果第一个条件不为空,那么 sql 语句为:update user u set u.username = ? ,u.sex = ? where id=?

choose

假如不想用到所有的查询条件,只要查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句

标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="selectUserByChoose" resultType="user" parameterType="user">
SELECT * FROM user
<where>
<choose>
<when test="id !='' and id != null">
id=#{id}
</when>
<when test="username !='' and username != null">
AND username=#{username}
</when>
<otherwise>
AND sex=#{sex}
</otherwise>
</choose>
</where>
</select>

有三个条件,id、username、sex,只能选择一个作为查询条件

  • 如果 id 不为空,那么查询语句为:select * from user where id=?

  • 如果 id 为空,那么看 username 是否为空

    • 如果不为空,那么语句为:select * from user where username=?
    • 如果 username 为空,那么查询语句为 select * from user where sex=?

trim

trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记的功能,自定义字符串截取

  • prefix:给拼串后的整个字符串加一个前缀,trim 标签体中是整个字符串拼串后的结果
  • prefixOverrides:去掉整个字符串前面多余的字符
  • suffix:给拼串后的整个字符串加一个后缀
  • suffixOverrides:去掉整个字符串后面多余的字符

改写 if + where 语句:

1
2
3
4
5
6
7
8
9
10
11
<select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User">
SELECT * FROM user
<trim prefix="where" prefixOverrides="and | or">
<if test="username != null">
AND username=#{username}
</if>
<if test="sex != null">
AND sex=#{sex}
</if>
</trim>
</select>

改写 if + set 语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 根据 id 更新 user 表的数据 -->
<update id="updateUserById" parameterType="com.ys.po.User">
UPDATE user u
<trim prefix="set" suffixOverrides=",">
<if test="username != null and username != ''">
u.username = #{username},
</if>
<if test="sex != null and sex != ''">
u.sex = #{sex},
</if>
</trim>
WHERE id=#{id}
</update>

foreach

基本格式:

1
2
3
4
<foreach>:循环遍历标签。适用于多个参数或者的关系。
<foreach collection=“”open=“”close=“”item=“”separator=“”>
获取参数
</foreach>

属性:

  • collection:参数容器类型, (list-集合, array-数组)
  • open:开始的 SQL 语句
  • close:结束的 SQL 语句
  • item:参数变量名
  • separator:分隔符

需求:循环执行 sql 的拼接操作,SELECT * FROM user WHERE id IN (1,2,5)

  • UserMapper.xml片段

    1
    2
    3
    4
    5
    6
    7
    8
    <select id="selectByIds" resultType="user" parameterType="list">
    SELECT * FROM student
    <where>
    <foreach collection="list" open="id IN(" close=")" item="id" separator=",">
    #{id}
    </foreach>
    </where>
    </select>
  • 测试代码片段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //4.获取StudentMapper接口的实现类对象
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<Integer> ids = new ArrayList<>();
    Collections.addAll(list, 1, 2);
    //5.调用实现类的方法,接收结果
    List<User> list = mapper.selectByIds(ids);

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

SQL片段

将一些重复性的 SQL 语句进行抽取,以达到复用的效果

格式:

1
2
<sql id=“片段唯一标识”>抽取的SQL语句</sql>		<!--抽取标签-->
<include refid=“片段唯一标识”/> <!--引入标签-->

使用:

1
2
3
4
5
6
7
8
9
10
<sql id="select">SELECT * FROM user</sql>

<select id="selectByIds" resultType="user" parameterType="list">
<include refid="select"/>
<where>
<foreach collection="list" open="id IN(" close=")" item="id" separator=",">
#{id}
</foreach>
</where>
</select>

逆向工程

MyBatis 逆向工程,可以针对单表自动生成 MyBatis 执行所需要的代码(mapper.java、mapper.xml、pojo…)

generatorConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<context id="testTables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatisrelation" userId="root"
password="root">
</jdbcConnection>

<!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL和NUMERIC类型解析为java.math.BigDecimal -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>

<!-- targetProject:生成PO类的位置!! -->
<javaModelGenerator targetPackage="com.ys.po"
targetProject=".\src">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- targetProject:mapper映射文件生成的位置!! -->
<sqlMapGenerator targetPackage="com.ys.mapper"
targetProject=".\src">
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<!-- targetPackage:mapper接口生成的位置,重要!! -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.ys.mapper"
targetProject=".\src">
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!-- 指定数据库表,要生成哪些表,就写哪些表,要和数据库中对应,不能写错! -->
<table tableName="items"></table>
<table tableName="orders"></table>
<table tableName="orderdetail"></table>
<table tableName="user"></table>
</context>
</generatorConfiguration>

生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void testGenerator() throws Exception{
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
//指向逆向工程配置文件
File configFile = new File(GeneratorTest.class.
getResource("/generatorConfig.xml").getFile());
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
callback, warnings);
myBatisGenerator.generate(null);

}

参考文章:https://www.cnblogs.com/ysocean/p/7360409.html


构建 SQL

基础语法

MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL 语句

方法 说明
SELECT(String… columns) 根据字段拼接查询语句
FROM(String… tables) 根据表名拼接语句
WHERE(String… conditions) 根据条件拼接语句
INSERT_INTO(String tableName) 根据表名拼接新增语句
INTO_VALUES(String… values) 根据值拼接新增语句
UPDATE(String table) 根据表名拼接修改语句
DELETE_FROM(String table) 根据表名拼接删除语句

增删改查注解:

  • @SelectProvider:生成查询用的 SQL 语句
  • @InsertProvider:生成新增用的 SQL 语句
  • @UpdateProvider:生成修改用的 SQL 语句注解
  • @DeleteProvider:生成删除用的 SQL 语句注解。
    • type 属性:生成 SQL 语句功能类对象
    • method 属性:指定调用方法

基本操作

  • MyBatisConfig.xml 配置

    1
    2
    3
    4
     <!-- mappers引入映射配置文件 -->
    <mappers>
    <package name="mapper"/>
    </mappers>
  • Mapper 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public interface StudentMapper {
    //查询全部
    @SelectProvider(type = ReturnSql.class, method = "getSelectAll")
    public abstract List<Student> selectAll();

    //新增数据
    @InsertProvider(type = ReturnSql.class, method = "getInsert")
    public abstract Integer insert(Student student);

    //修改操作
    @UpdateProvider(type = ReturnSql.class, method = "getUpdate")
    public abstract Integer update(Student student);

    //删除操作
    @DeleteProvider(type = ReturnSql.class, method = "getDelete")
    public abstract Integer delete(Integer id);

    }
  • ReturnSQL 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class ReturnSql {
    //定义方法,返回查询的sql语句
    public String getSelectAll() {
    return new SQL() {
    {
    SELECT("*");
    FROM("student");
    }
    }.toString();
    }

    //定义方法,返回新增的sql语句
    public String getInsert(Student stu) {
    return new SQL() {
    {
    INSERT_INTO("student");
    INTO_VALUES("#{id},#{name},#{age}");
    }
    }.toString();
    }

    //定义方法,返回修改的sql语句
    public String getUpdate(Student stu) {
    return new SQL() {
    {
    UPDATE("student");
    SET("name=#{name}","age=#{age}");
    WHERE("id=#{id}");
    }
    }.toString();
    }

    //定义方法,返回删除的sql语句
    public String getDelete(Integer id) {
    return new SQL() {
    {
    DELETE_FROM("student");
    WHERE("id=#{id}");
    }
    }.toString();
    }
    }
  • 功能实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public class SqlTest {	
    @Test //查询全部
    public void selectAll() throws Exception{
    //1.加载核心配置文件
    InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

    //2.获取SqlSession工厂对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

    //3.通过工厂对象获取SqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession(true);

    //4.获取StudentMapper接口的实现类对象
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

    //5.调用实现类对象中的方法,接收结果
    List<Student> list = mapper.selectAll();

    //6.处理结果
    for (Student student : list) {
    System.out.println(student);
    }

    //7.释放资源
    sqlSession.close();
    is.close();
    }

    @Test //新增
    public void insert() throws Exception{
    //1 2 3 4获取StudentMapper接口的实现类对象
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

    //5.调用实现类对象中的方法,接收结果 ->6 7
    Student stu = new Student(4,"赵六",26);
    Integer result = mapper.insert(stu);
    }

    @Test //修改
    public void update() throws Exception{
    //1 2 3 4 5调用实现类对象中的方法,接收结果 ->6 7
    Student stu = new Student(4,"赵六wq",36);
    Integer result = mapper.update(stu);
    }
    @Test //删除
    public void delete() throws Exception{
    //1 2 3 4 5 6 7
    Integer result = mapper.delete(4);
    }
    }

插件使用

插件原理

实现原理:插件是按照插件配置顺序创建层层包装对象,执行目标方法的之后,按照逆向顺序执行(栈)

在四大对象创建时:

  • 每个创建出来的对象不是直接返回的,而是 interceptorChain.pluginAll(parameterHandler)
  • 获取到所有 Interceptor(插件需要实现的接口),调用 interceptor.plugin(target)返回 target 包装后的对象
  • 插件机制可以使用插件为目标对象创建一个代理对象,代理对象可以拦截到四大对象的每一个执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor{

//intercept:拦截目标对象的目标方法的执行
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin...intercept:" + invocation.getMethod());
//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询11号员工
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象:" + target);
//拿到:StatementHandler==>ParameterHandler===>parameterObject
//拿到target的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句用的参数是:" + value);
//修改完sql语句要用的参数
metaObject.setValue("parameterHandler.parameterObject", 11);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}

// plugin:包装目标对象的,为目标对象创建一个代理对象
@Override
public Object plugin(Object target) {
//可以借助 Plugin 的 wrap 方法来使用当前 Interceptor 包装我们目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象" + target);
Object wrap = Plugin.wrap(target, this);
//返回为当前target创建的动态代理
return wrap;
}

// setProperties:将插件注册时的property属性设置进来
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息:" + properties);
}
}

核心配置文件:

1
2
3
4
5
6
7
<!--plugins:注册插件  -->
<plugins>
<plugin interceptor="mybatis.dao.MyFirstPlugin">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</plugin>
</plugins>

分页插件

  • 分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。
  • MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。
  • PageHelper:第三方分页助手,将复杂的分页操作进行封装,从而让分页功能变得非常简单

分页操作

开发步骤:

  1. 导入 PageHelper 的 Maven 坐标

  2. 在 MyBatis 核心配置文件中配置 PageHelper 插件

    注意:分页助手的插件配置在通用 Mapper 之前

    1
    2
    3
    4
    5
    6
    7
    <plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
    <!-- 指定方言 -->
    <property name="dialect" value="mysql"/>
    </plugin>
    </plugins>
    <mappers>.........</mappers>
  3. 与 MySQL 分页查询页数计算公式不同

    static <E> Page<E> startPage(int pageNum, int pageSize):pageNum第几页,pageSize页面大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void selectAll() {
    //第一页:显示2条数据
    PageHelper.startPage(1,2);
    List<Student> students = sqlSession.selectList("StudentMapper.selectAll");
    for (Student student : students) {
    System.out.println(student);
    }
    }

参数获取

PageInfo构造方法:

  • PageInfo<Student> info = new PageInfo<>(list) : list 是 SQL 执行返回的结果集合,参考上一节

PageInfo相关API:

  1. startPage():设置分页参数
  2. PageInfo:分页相关参数功能类。
  3. getTotal():获取总条数
  4. getPages():获取总页数
  5. getPageNum():获取当前页
  6. getPageSize():获取每页显示条数
  7. getPrePage():获取上一页
  8. getNextPage():获取下一页
  9. isIsFirstPage():获取是否是第一页
  10. isIsLastPage():获取是否是最后一页