Mybatis学习笔记,Mybatis一二级缓存的理解

Oracle中分页查询因为存在伪列rownum,sql语句写起来较为复杂,现在介绍一种通过使用MyBatis中的RowBounds进行分页查询,非常方便。

图片 1

   
频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。

使用MyBatis中的RowBounds进行分页查询时,不需要在 sql 语句中写
offset,limit,mybatis 会自动拼接 分页sql ,添加
offset,limit,实现自动分页。

官方文档

  • ### MyBatis的缓存分为两种:

需要前台传递参数currentPage和pageSize两个参数,分别是当前页和每页数量,controller层把参数传递给service层即可,下面是service实现的代码:

  • 简介
  • 入门
  • XML配置
  • XML映射文件
  • 动态SQL
  • Java
    API
  • SQL语句构建器
  • 日志
  1. 一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库
  2. 二级缓存,二级缓存是Mapper级别的缓存,定义在Mapper文件的<cache>标签中并需要开启此缓存,多个Mapper文件可以共用一个缓存,依赖<cache-ref>标签配置
package com.xyfer.service.impl;import java.util.HashMap;import java.util.List;import java.util.Map;import org.apache.ibatis.session.RowBounds;import com.xyfer.dao.UserDao;import com.xyfer.service.UserService;public class UserServiceImpl implements UserService { private UserDao userDao; @Override public MapString, Object queryUserList(String currentPage, String pageSize) { //查询数据总条数 int total = userDao.queryCountUser(); //返回结果集 MapString,Object resultMap = new HashMapString,Object(); resultMap.put("total", total); //总页数 int totalpage = (total + Integer.parseInt(pageSize) - 1) / Integer.parseInt(pageSize); resultMap.put("totalpage", totalpage); //数据的起始行 int offset = (Integer.parseInt(currentPage)-1)*Integer.parseInt(pageSize); RowBounds rowbounds = new RowBounds(offset, Integer.parseInt(pageSize)); //用户数据集合 ListMapString, Object userList = userDao.queryUserList(rowbounds); resultMap.put("userList", userList); return resultMap; }}

图片 2

    

dao层接口:

一、 JDBC回顾

SqlSession>DefaultSqlSession(selectList)>this.executor.query>Executor(一级缓存BaseExecutor,
二级缓存CachingExecutor)

package com.xyfer.dao;import java.util.List;import java.util.Map;import org.apache.ibatis.session.RowBounds;public interface UserDao { public int queryCountUser(); //查询用户总数 public ListMapString, Object queryUserList(RowBounds rowbounds); //查询用户列表}
JDBC的使用过程:

register the JDBC driver 加载驱动
open a connection 打开连接
execute using statment, including binding params
执行statment并绑定参数
extract data from result set (only select) 从结果集中获取数据
close database resources 关闭连接

CacheKey判断两次查询条件是否一致。

对应的mapper.xml文件:

JDBC具有的问题:

代码与sql语句耦合
动态组装sql语句处理繁琐
数据对象与结果集的映射与代码耦合
需要关心数据库资源的释放

 

xml version="1.0" encoding="UTF-8" !DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"""mapper namespace="com.xyfer.mapper.UserMapper" !-- 查询用户总数 -- select resultType="java.lang.Integer" select count(1) from user /select !-- 查询用户列表 -- select resultType="java.util.Map" select * from user /select/mapper

二、 MyBatis的简介

MyBatis是一个支持普通
SQL查询,存储过程和高级映射的优秀持久层框架;MyBatis
消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis
使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Old
Java Objects,普通的 Java对象)映射成数据库中的记录。

一级缓存:/executor/BaseExecutor.class

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if(this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }
​
            List list;
            try {
                ++this.queryStack;
                list = resultHandler == null?(List)this.localCache.getObject(key):null;
                if(list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }
​
            if(this.queryStack == 0) {
                Iterator i$ = this.deferredLoads.iterator();
​
                while(i$.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
                    deferredLoad.load();
                }
​
                this.deferredLoads.clear();
                if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }
​
            return list;
        }
    }

通过postman调用接口,传入对应的参数,即可实现分页查询数据。

依赖
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
</dependency>

 

总结

术语解释
  • SqlSessionFactoryBuilder

  • SqlSessionFactory

  • SqlSession
    (1)每个基于 MyBatis 的应用都是以一个 SqlSessionFactory
    的实例为中心的。SqlSessionFactory 的实例可以通过
    SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从
    XML 配置文件或一个预先定制的 Configuration 的实例构建出
    SqlSessionFactory 的实例。
    (2)SqlSessionFactory可以在xml配置文件中构建,也可以使用代码构建。
    (3)三者的作用域(Scope)和生命周期:不同作用域和生命周期类是至关重要的,因为错误的使用会导致非常严重的并发问题:
    SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了
    SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder
    实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用
    SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory
    实例,但是最好还是不要让其一直存在以保证所有的 XML
    解析资源开放给更重要的事情。
    SqlSessionFactory:SqlSessionFactory
    一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用
    SqlSessionFactory
    的最佳实践是在应用运行期间不要重复创建多次,多次重建
    SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此
    SqlSessionFactory
    的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式
    SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession
    的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将
    SqlSession
    实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将
    SqlSession 实例的引用放在任何类型的管理作用域中,比如 Serlvet
    架构中的 HttpSession。如果你现在正在使用一种 Web 框架,要考虑
    SqlSession 放在一个和 HTTP
    请求对象相似的作用域中。换句话说,每次收到的 HTTP
    请求,就可以打开一个
    SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到
    finally 块中以确保每次都能执行关闭。

  • 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>
          <environments default="development">
              <environment id="development">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <property name="driver" value="${driver}"/>
                      <property name="url" value="${url}"/>
                      <property name="username" value="${username}"/>
                      <property name="password" value="${password}"/>
                  </dataSource>
              </environment>
          </environments>
          <mappers>
              <mapper resource="org/mybatis/example/BlogMapper.xml"/>
          </mappers>
      </configuration>
    
  • sql mapper文件(存放需要执行的sql语句)

一级缓存三个结论:

  1. MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor
  2. MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为”localCache”的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果
  3. MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将<select>标签中的flushCache属性设置为true即可,这意味着每次查询的时候都会清理一遍PerpetualCache,PerpetualCache中没数据,自然只能走DB。增删改没有一级缓存。

 

以上所述是小编给大家介绍的Oracle使用MyBatis中RowBounds实现分页查询功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

三、 XML配置文件

Mybatis使用xml配置文件主要有两个地方:Mybatis配置和Mapper映射配置。具体可以参考官方文档,说得非常详细。

properties
配置属性

settings
这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis
的运行时行为,比如是否使用缓存、是否延迟加载、是否允许单一语句返回多结果集、是否允许
JDBC 支持自动生成主键等;一个例子:

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

typeAliases
类型别名是为 Java 类型设置一个短的名字。它只和 XML
配置有关,存在的意义仅在于用来减少类完全限定名的冗余。

typeHandlers
用来指定所使用的类型处理器。无论是 MyBatis
在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,
都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

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

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

environments(配置环境)
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL
映射应用于多种数据库之中,
现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者共享相同
Schema 的多个生产数据库, 想使用相同的 SQL 映射。许多类似的用例。
不过要记住:尽管可以配置多个环境,每个 SqlSessionFactory
实例只能选择其一。

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

//缓存判断查询条件是否一致:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());//判断id属性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getOffset()));//判断Offset属性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//判断Limit属性是否相同
            cacheKey.update(boundSql.getSql());//判断sql属性是否相同
            List parameterMappings = boundSql.getParameterMappings();//后面都是判断参数是否相同
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
​
            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    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 = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
​
                    cacheKey.update(value);
                }
            }
​
            return cacheKey;
        }
    }

 

 

 

 

四、XML映射文件

MyBatis
的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的
XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC
代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL
构建的,并且比普通的方法做的更好。

select
select 元素有很多属性允许你配置,来决定每条语句的作用细节。

<select
        id="selectPerson" ->> 在命名空间中唯一的标识符,可以被用来引用这条语句
        parameterType="int" ->> 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
        resultType="hashmap" ->> 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用。
        resultMap="personResultMap" ->> 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用。
        flushCache="false" ->> 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。
        useCache="true" ->> 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true
        timeout="10000" ->> 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
        fetchSize="256" ->> 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。
        statementType="PREPARED" ->> STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
        resultSetType="FORWARD_ONLY" ->> FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
        >

insert, update 和 delete
数据变更语句 insert,update 和 delete 的实现非常接近:

<insert
    id="insertAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    keyProperty=""
    keyColumn=""
    useGeneratedKeys=""
    timeout="20"></insert>

<update
    id="updateAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    timeout="20"></update>

<delete
    id="deleteAuthor"
    parameterType="domain.blog.Author"
    flushCache="true"
    statementType="PREPARED"
    timeout="20"></delete>

如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。

  • 如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL
    Server),那么你可以设置 useGeneratedKeys=”true”,然后再把
    keyProperty 设置到目标属性上就OK了。例如:

    <insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
      insert into Author (username,password,email,bio)
      values (#{username},#{password},#{email},#{bio})
    </insert>
    

这样就可以将自动生成的主键绑定到Author对象的id属性上了。

  • 如果你的数据库还支持多行插入,
    你也可以传入一个Authors数组或集合,并返回自动生成的主键。

    <insert id="insertAuthor" useGeneratedKeys="true"  keyProperty="id">
      insert into Author (username, password, email, bio) values
      <foreach item="item" collection="list" separator=",">
        (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
      </foreach>
    </insert>
    

sql
这个元素可以被用来定义可重用的 SQL
代码段
,可以包含在其他语句中。它可以被静态地(在加载参数) 参数化.
不同的属性值通过包含的实例变化. 比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

这个 SQL 片段可以被包含在其他语句中,例如:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

参数(Parameters)
参数是 MyBatis 非常强大的功能;

  • 例子1:

    <select id="selectUsers" resultType="User">
      select id, username, password
      from users
      where id = #{id}
    </select>
    

上面的这个示例说明了一个非常简单的命名参数映射。参数类型被设置为
int,这样这个参数就可以被设置成任何内容。

  • 例子2:

    <insert id="insertUser" parameterType="User">
      insert into users (id, username, password)
      values (#{id}, #{username}, #{password})
    </insert>
    

如果 User 类型的参数对象传递到了语句中,id、username 和 password
属性将会被查找,然后将它们的值传入预处理语句的参数中。这点对于向语句中传参是比较好的而且又简单,不过参数映射的功能远不止于此。

  • 参数的属性

像 MyBatis 的其他部分一样,参数也可以指定一个特殊的数据类型。

  例子1 : #{property,javaType=int,jdbcType=NUMERIC}
  例子2 : #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
  例子3 :#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

  javaType 通常可以从参数对象中来去确定,前提是只要对象不是一个 HashMap。那么 javaType 应该被确定来保证使用正确类型处理器。尽管看起来配置变得越来越繁琐,但实际上是很少去设置它们。
  numericScale 对于数值类型,还有一个小数保留位数的设置,来确定小数点后保留的位数。
  mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变,就像你在获取输出参数时所期望的那样。如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 来映射结果集到参数类型。要注意这里的 javaType 属性是可选的,如果左边的空白是 jdbcType 的 CURSOR 类型,它会自动地被设置为结果集。
  • 字符串替换

默认情况下,使用#{}格式的语法会导致 MyBatis
创建预处理语句属性并安全地设置值(比如?)。这样做更安全,更迅速,通常也是首选做法,不过有时你只是想直接在
SQL 语句中插入一个不改变的字符串。比如,像 ORDER BY,你可以这样来使用:

  ORDER BY ${columnName}
  这里 MyBatis 不会修改或转义字符串。

注意:以这种方式接受从用户输出的内容并提供给语句中不变的字符串是不安全的,会导致潜在的
SQL 注入攻击
,因此要么不允许用户输入这些字段,要么自行转义并检验。

Result Maps
resultMap 元素是 MyBatis 中最重要最强大的元素。它就是让你远离
90%的需要从结果集中取出数据的 JDBC 代码的那个东西,
而且在一些情形下允许你做一些 JDBC 不支持的事情。 事实上,
编写相似于对复杂语句联合映射这些等同的代码, 也许可以跨过上千行的代码。
ResultMap
的设计就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们的关系。

resultMap用来映射查询结果,不论是否在标签属性中显示的指定,MyBatis
会在幕后自动创建一个 ResultMap,基于属性名来映射列到 JavaBean 的属性上。

我将其概括为3种形式:

  • (1)指定resultType为map,将所有列被自动映射到 HashMap 的键上:
    <select id=”selectUsers” resultType=”map”>
    select id, username, hashedPassword
    from some_table
    where id = #{id}
    </select>
    这在很多情况下是有用的,但是 HashMap
    不能很好描述一个领域模型,所以我们还有以下几种映射方式。

  • (2)指定resultType为JavaBean或
    POJO,查询结果自动映射到java类属性上,映射规则是基于属性名来映射列到
    JavaBean 的属性上

    <select id=”selectUsers”
    resultType=”com.someapp.model.User”>
    select id, username, hashedPassword
    from some_table
    where id = #{id}
    </select>
    也许会受限于映射规则,不过如果列名没有精确匹配,你可以在列名上使用
    select 字句的别名(AS,一个基本的SQL特性)来匹配标签:
    <select id=”selectUsers” resultType=”User”>
    select
    user_id as “id”,
    user_name as “userName”,
    hashed_password as “hashedPassword”
    from some_table
    where id = #{id}
    </select>

  • (3)使用外部的 resultMap,这也是解决列名不匹配的另外一种方式

    1. 定义外部resultMap与实体之间的映射关系
      <resultMap id=”userResultMap” type=”User”>
      <id property=”id” column=”user_id” />
      <result property=”username” column=”user_name”/>
      <result property=”password” column=”hashed_password”/>
      </resultMap>

      2.引用它的语句使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:

—— “如果世界总是这么简单就好了”

高级结果映射

MyBatis的构想:数据库不用永远是你想要的或需要它们是什么样的。而我们
最喜欢的数据库最好是第三范式或 BCNF
模式,但它们有时不是。如果可能有一个单独的
数据库映射,所有应用程序都可以使用它,这是非常好的,但有时也不是。结果映射就是
MyBatis 提供处理这个问题的答案。

具体地说,当我们查询的结果非常复杂,比如说使用了连接查询,那么查询到的结果该如何映射?

【例子】:比如有下面这样一个查询需求,需要查询一篇博客和它的作者信息、评论列表以及每条评论的回复列表,下面是具体的查询语句:

<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
    select
    B.id as blog_id,
    B.title as blog_title,
    B.author_id as blog_author_id,
    A.id as author_id,
    A.username as author_username,
    A.password as author_password,
    A.email as author_email,
    A.bio as author_bio,
    A.favourite_section as author_favourite_section,
    P.id as post_id,
    P.blog_id as post_blog_id,
    P.author_id as post_author_id,
    P.created_on as post_created_on,
    P.section as post_section,
    P.subject as post_subject,
    P.draft as draft,
    P.body as post_body,
    C.id as comment_id,
    C.post_id as comment_post_id,
    C.name as comment_name,
    C.comment as comment_text,
    T.id as tag_id,
    T.name as tag_name
    from Blog B
    left outer join Author A on B.author_id = A.id
    left outer join Post P on B.id = P.blog_id
    left outer join Comment C on P.id = C.post_id
    left outer join Post_Tag PT on PT.post_id = P.id
    left outer join Tag T on PT.tag_id = T.id
    where B.id = #{id}
</select>

你可能想把它映射到一个智能的对象模型,包含一个作者写的博客,有很多的博文,每
篇博文有零条或多条的评论和标签,就像这样:

class Blog
    - id
    - title
    - class Author
        - id
        - username
        - password
        - email
        - bio
        - favouriteSection
    - List class Post
        - id
        - subject
        - class Author
            - ...
        - List Comment
            - id
        - List class Tag
            - id
    - draft(是否是草稿)

下面是一个完整的复杂结果映射例子 (假设Author, Blog, Post,
Comment和Tag都是类型的别名):

<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
        <idArg column="blog_id" javaType="int"/>
    </constructor>
    <result property="title" column="blog_title"/>
    <association property="author" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="username" column="author_username"/>
        <result property="password" column="author_password"/>
        <result property="email" column="author_email"/>
        <result property="bio" column="author_bio"/>
        <result property="favouriteSection" column="author_favourite_section"/>
    </association>
    <collection property="posts" ofType="Post">
        <id property="id" column="post_id"/>
        <result property="subject" column="post_subject"/>
        <association property="author" javaType="Author"/>
        <collection property="comments" ofType="Comment">
            <id property="id" column="comment_id"/>
        </collection>
        <collection property="tags" ofType="Tag" >
            <id property="id" column="tag_id"/>
        </collection>
        <discriminator javaType="int" column="draft">
            <case value="1" resultType="DraftPost"/>
        </discriminator>
    </collection>
</resultMap>

这就是resultMap的强大作用!将我们的复杂查询映射到智能实体对象。
下面是 resultMap 元素的概念视图:

图片 3

上述标签中可以具有的属性:

图片 4

图片 5

  • jdbcType:支持的 JDBC 类型
  • constructor:构造方法
  • association:关联
    • 关联的嵌套查询
    • 关联的嵌套结果
  • collection:集合
    • 集合的嵌套查询
    • 集合的嵌套结果
  • discriminator:鉴别器

一级缓存从四组共五个条件判断两次查询是相同的:

  1. <select>标签所在的Mapper的Namespace+<select>标签的id属性
  2. RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
  3. <select>标签中定义的sql语句
  4. 输入参数的具体参数值,一个int值就update一个int,一个String值就update一个String,一个List就轮询里面的每个元素进行update

即只要两次查询满足以上三个条件且没有定义flushCache=”true”,那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。

假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存

MyBatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。

从这个角度考虑,为了避免MyBatis二级缓存中数据量过大导致内存溢出,MyBatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。

 

五、动态SQL

动态SQL

MyBatis 的强大特性之一便是它的动态 SQL功能。如果你有使用 JDBC
或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL
语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态
SQL 这一特性可以彻底摆脱这种痛苦。

通常使用动态 SQL 不可能是独立的一部分,MyBatis 当然使用一种强大的动态 SQL
语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中

动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。在 MyBatis
之前的版本中,有很多的元素需要来了解。MyBatis 3
大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis
采用功能强大的基于 OGNL 的表达式来消除其他元素。

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

二级缓存:/executor/CachingExecutor.class

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();//Cache是从MappedStatement中获取到的,而MappedStatement又和每一个<insert>、<delete>、<update>、<select>绑定并在MyBatis启动的时候存入Configuration中:
        if(cache != null) {
            this.flushCacheIfRequired(ms);//根据flushCache=true或者flushCache=false判断是否要清理二级缓存
            if(ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);//保证MyBatis二级缓存不会存储存储过程的结果
                List list = (List)this.tcm.getObject(cache, key);//tcm装饰器模式,创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的<cache>创建的Cache实例
                if(list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }//如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去
​
                return list;
            }
        }

//优先读取以上二级缓存,query方法优先读取默认实现的一级缓存。

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

    }

 

 

 

 

MyBatis支持三种类型的二级缓存:

  • MyBatis默认的缓存,type为空,Cache为PerpetualCache
  • 自定义缓存
  • 第三方缓存

 

开启二级缓存:

<settings>//mybatis.cfg.xml
        <!-- 开启二级缓存  默认值为true -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

<mapper namespace="cn.sxt.vo.user.mapper">//mapper.xml
    <!-- 开启本mapper namespace下的二级缓存 -->
    <cache></cache>

 

 

存在问题:

select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

对于tableA与tableB的操作定义在两个Mapper中,分别叫做MapperA与MapperB,即它们属于两个命名空间,如果此时启用缓存:

  1. MapperA中执行上述sql语句查询这6个字段
  2. tableB更新了col1与col2两个字段
  3. MapperA再次执行上述sql语句查询这6个字段(前提是没有执行过任何insert、delete、update操作)

此时问题就来了,即使第(2)步tableB更新了col1与col2两个字段,第(3)步MapperA走二级缓存查询到的这6个字段依然是原来的这6个字段的值,因为我们从CacheKey的3组条件来看:

  1. <select>标签所在的Mapper的Namespace+<select>标签的id属性
  2. RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
  3. <select>标签中定义的sql语句

对于MapperA来说,其中的任何一个条件都没有变化,自然会将原结果返回。

这个问题对于MyBatis的二级缓存来说是一个无解的问题,因此使用MyBatis二级缓存有一个前提:

必须保证所有的增删改查都在同一个命名空间下才行

六、基于注解方式的使用

注解方式又将SQL和Java代码耦合,不推荐使用。但需要掌握一些基本的注解,以更好的配合基于Mapper文件的使用。

@Param

注解对象:方法参数
作用:如果你的映射器的方法需要多个参数, 这个注解可以被应用于映射器的方法
参数来给每个参数一个名字。否则,多 参数将会以它们的顺序位置来被命名
(不包括任何 RowBounds 参数) 比如。 #{param1} , #{param2} 等 , 这 是 默
认 的 。 使 用 @Param(“person”),参数应该被命名为 #{person}。

七、 集成Spring

依赖
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
</dependency>

将Mybatis与Spring集成,可以利用Spring的依赖注入对Mybatis进行托管,
SQLSessionFactory、SqlSession的管理都可以被Spring管理。

图片 6

0、引入properties
<context:property-placeholder location="jdbc.properties" ignore-unresolvable="true"/>
1、配置数据源
我们这里使用druid
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${url}"/>
    <property name="username" value="${user}"/>
    <property name="password" value="${password}"/>
    <property name="filters" value="stat"/>
    <property name="maxActive" value="20"/>
    <property name="initialSize" value="1"/>
    <property name="maxWait" value="60000"/>
    <property name="minIdle" value="1"/>
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <property name="validationQuery" value="SELECT 'x'"/>
    <property name="testWhileIdle" value="true"/>
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>
    <property name="poolPreparedStatements" value="true"/>
    <property name="maxOpenPreparedStatements" value="20"/>
</bean>
2、配置SqlSessionFactoryBean
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

configLocation:mybatis配置文件路径
mapperLocations:sql mapper文件路径

3、配置MapperScannerConfigurer

配置要扫描的DAO接口(与xml
mapper对应)的包位置,这个包下面的接口将会被扫描,我们在Service中可以通过自动装配的方法获取并使用

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.qunar.frc.demo3.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

sqlSessionFactoryBeanName:指定SqlSessionFactoryBean

4、将数据源DataSource的事务托管给Spring
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
# 开启注解事务管理
<tx:annotation-driven transaction-manager="transactionManager"/>
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>
    <settings>
        <!-- Globally enables or disables any caches configured in any mapper under this configuration -->
        <setting name="cacheEnabled" value="false"/>
        <!-- Sets the number of seconds the driver will wait for a response from the database -->
        <setting name="defaultStatementTimeout" value="3000"/>
        <!-- Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- Allows JDBC support for generated keys. A compatible driver is required.
        This setting forces generated keys to be used if set to true,
         as some drivers deny compatibility but still work -->
        <setting name="useGeneratedKeys" value="true"/>
    </settings>

    <!-- 为这个包下面的实体类起别名 -->
    <typeAliases>
        <package name="com.xxx.frc.demo3.model"/>
    </typeAliases>

    <!-- 自定义的类型处理器 -->
    <typeHandlers>
        <typeHandler handler="com.xxx.frc.demo3.typehandler.MoneyTypeHandler" javaType="xxx.api.pojo.Money"/>
        <typeHandler handler="com.xxx.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler" javaType="com.xxx.frc.demo3.enums.LevelEnum"/>
    </typeHandlers>

    <!-- 事务拦截 -->
    <plugins>
        <plugin interceptor="com.xxx.base.meerkat.orm.mybatis.support.ResultSetHandlerInterceptor"/>
        <plugin interceptor="com.xxx.base.meerkat.orm.mybatis.support.StatementHandlerInterceptor"/>
        <plugin interceptor="com.xxx.frc.demo3.plugin.SlowQueryTimePlugin">
            <property name="slowMillisecond" value="1"/>
        </plugin>
    </plugins>

</configuration>
6、mapper配置文件

八、 typeHandler

无论是 MyBatis
在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成
Java 类型。Mybatis默认为我们实现了许多TypeHandler,
当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。

那么,Mybatis为我们实现了哪些TypeHandler呢?
我们怎么自定义实现一个TypeHandler ?

从源码看TypeHandler的实现和管理

org.apache.ibatis.type.TypeHandlerRegistry是TypeHandler的注册管理类,在这里注册了所有Mybatis提供的默认类型处理器:

图片 7

TypeHandlerRegistry()方法中注册默认提供的类型处理器

由源码可以看到,
mybatis默认实现了很多TypeHandler,继承自一个抽象类:BaseTypeHandler,自定义typeHandler需要实现4个抽象方法:

图片 8

自定义TypeHandler——以CodeEnumTypeHandler为例

在这里我参考Qunar对枚举的类型处理封装的类型处理器CodeEnumTypeHandler,用于处理枚举类型。在很多场景下,数据库中需要枚举变量作为字段值,比如性别(1-男,2-女)等。

为了适用所有使用Enum的场景,强制规定:使用CodeEnumTypeHandler的Enum必须具有两个方法:

code和静态的codeOf方法:
public int code() {
  return this.getId();
  }

public static xxxEnum codeOf(int id) {
  return map.get(id);
}

code()方法:为一个Enum变量标识了编号,通过code()获取该Enum的编号,当然这个编号的意义必须与数据库中的意义相同,这用codeOf方法获得的枚举才有意义
codeOf()方法:通过编号获取Enum

在CodeEnumTypeHandler中,通过反射获取到Enum的这两个方法,在继承自BaseTypeHandler的四个方法中invoke调用,从而实现从Integer
->> Enum的转换。

最后,要在mybatis-config.xml中注册typeHandler:

<typeHandlers>
    <typeHandler handler="com.xxx.xxx.CodeEnumTypeHandler" javaType="com.demo3.enums.LevelEnum"/>
</typeHandlers>

这样,凡是LevelEnum类型,都将使用CodeEnumTypeHandler来处理。


根据上面的思路,我这里自己实现一个枚举类型的类型处理器,并且实现一个表示性别的枚举类:

/**
 * version    date      author
 * ──────────────────────────────────
 * 1.0       17-3-22   wanlong.ma
 * Description: 性别枚举类
 * Others:
 * Function List:
 * History:
 */
public enum  SexEnum {
    MALE(1,"男"),
    FEMALE(2,"女");

    private int id;         // 标识号
    private String type;    // 类型

    SexEnum(int id, String type) {
        this.id = id;
        this.type = type;
    }

    public int getId() {
        return id;
    }

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

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    /////////定义下面的属性和方法用于类型处理//////////

    private static final Map<Integer, SexEnum> map = Maps.newHashMap();
    static {
        for(SexEnum sexEnum : values()) {
            map.put(sexEnum.getId(), sexEnum);
        }
    }

    public int code(){
        return this.getId();
    }

    public static SexEnum codeOf(Integer id) {
        return map.get(id);
    }

}

/**
 * version    date      author
 * ──────────────────────────────────
 * 1.0       17-3-22   wanlong.ma
 * Description: 参考om.qunar.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler写的Enum通用类型处理器
 *              使用该处理器的Enum必须自己实现一个code方法和静态的codeOf方法
 * Others:
 * Function List:
 * History:
 */
public class CustomEnumTypeHandler extends BaseTypeHandler<Enum<?>> {
    private Method code;
    private Method codeOf;

    public CustomEnumTypeHandler(Class<Enum<?>> enumType) {
        String className = enumType.getName();
        String simpleName = enumType.getSimpleName();

        try {
            code = enumType.getDeclaredMethod("code");
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Method " + className + "#code():int required.'");
        }

        try {
            codeOf = enumType.getDeclaredMethod("codeOf", int.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
        }

        if (!Modifier.isStatic(codeOf.getModifiers())) {
            throw new RuntimeException("Static method " + className + "#codeOf(int code):" + simpleName + " required.");
        }
    }

    /**
     * 调用enum的code方法,返回枚举的标识
     * @param object
     * @return
     */
    private Integer code(Enum object){
        try {
            return (Integer) code.invoke(object);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    /**
     * 根据枚举标识号获取枚举值
     * 注意codeOf方法在枚举类型中是一个静态方法,所以此处是静态调用
     * @param value
     * @return
     */
    private Enum codeOf(int value){
        try {
            return (Enum) codeOf.invoke(null, value);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Enum<?> parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, code(parameter));
    }

    @Override
    public Enum<?> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return codeOf(rs.getInt(columnName));
    }

    @Override
    public Enum<?> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return codeOf(rs.getInt(columnIndex));
    }

    @Override
    public Enum<?> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return codeOf(cs.getInt(columnIndex));
    }
}

配置文件中注册:

<typeHandlers>
    <typeHandler handler="com.xxx.db.handler.CustomEnumTypeHandler" javaType="com.xxx.db.enums.SexEnum"/>
</typeHandlers>

走起走起~

九、Plugins

十、RowBounds与分页

参考:
MyBatis中的RowBounds
mybatis的两种分页方式:RowBounds和PageHelper

Mybatis如何分页查询?Mysql中可以使用limit语句,但limit并不是标准SQL中的,如果是其它的数据库,则需要使用其它语句。MyBatis提供了RowBounds类,用于实现分页查询。通过设置RowBounds中的两个变量来设置分页起始行和页面大小:offset和limit。

使用方法
  • RowBounds:在mapper.java中的方法中传入RowBounds对象。

    RowBounds rowBounds = new RowBounds(offset, page.getPageSize()); // offset起始行,limit是当前页显示多少条数据
    public List<ProdProduct> findRecords(HashMap<String,Object> map,RowBounds rowBounds);
    
  • mappep.xml里面正常配置,不用对rowBounds任何操作。mybatis的拦截器自动操作rowBounds进行分页

从源码分析RowBounds的实现原理
  • RowBounds类
![](https://upload-images.jianshu.io/upload_images/1844198-3bee6691a667ba32.png)

RowBounds类
  • DefaultResultSetHandler类中通过RowBounds实现的分页

图片 9

handleRowValuesForSimpleResultMap方法

图片 10

shouldProcessMoreRows方法

RowBounds在处理分页时,只是简单的把offset之前的数据都skip掉,超过limit之后的数据不取出,上图中的代码取自MyBatis中的DefaultResultSetHandler类。跳过offset之前的数据是由方法skipRows处理,判断数据是否超过了limit则是由shouldProcessMoreRows方法进行判断。说简单点,就是先把数据全部查询到ResultSet,然后从ResultSet中取出offset和limit之间的数据,这就实现了分页查询。

参考另外一种分页方法:Mybatis 数据库物理分页插件
PageHelper

十一、关于DataSource

十二、Spring+Mybatis通用dao层、service层的实现原则

spring+mybatis通用dao层、service层的一些个人理解与实现

十三、其他

sql中使用转义字符

在mapper ***.xml中的sql语句中,不能直接用大于号、小于号要用转义字符:

图片 11

MyBatis中Like语句使用方式

mysql数据库:

SELECT  
*  
FROM  
user  
WHERE  
name like CONCAT('%',#{name},'%')  
如果select查询为空,Mybatis会返回什么
/**
 * 验证结果:返回null
 */
@Test
public void testSelectIfNotExist(){
    EmployeeModel employeeModel = employeeService.queryEmployeeByStaffId(999);
    logger.info("Is employeeModel null? ->> {}", employeeModel == null);
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注