《MyBatis从入门到精通》读书笔记

401 阅读14分钟

MyBatis算是Java技术栈中必不可少的一员,虽然项目中也在用,但是也只是简单使用而已,除此之外的知识来源就是通读了官方文档。断断续续花两三周时间看完《MyBatis从入门到精通》这本书,总体评价正如豆瓣的一个短评所言“不错的mybatis 入门书。全书只讲入门,跟精通是没关系的”,所以打算用这本作为梳理知识之用,后期辅以其他书籍深入

全书结构

本书从一个简单的MyBatis查询入手,搭建起学习MyBatis的基础开发环境。通过全面的示例代码和测试讲解了在MyBatis XML方式和注解方式中进行增、删、改、查操作的基本用法,介绍了动态SQL在不同方面的应用以及在使用过程中的最佳实践方案。针对MyBatis高级映射、存储过程和类型处理器提供了丰富的示例,通过自下而上的方法使读者更好地理解和掌握MyBatis的高级用法,同时针对MyBatis的代码生成器提供了详细的配置介绍。此外,本书还提供了缓存配置、插件开发、Spring、Spring Boot集成的详细内容。最后通过介绍Git和GitHub让读者了解MyBatis开源项目,通过对MyBatis源码和测试用例的讲解让读者更好掌握MyBatis

第1章 MyBatis入门,简单介绍MyBatis发展历程和相关背景,并搭建基础环境,作为后续章节的基础
第2章 MyBatis XML方式的基本用法,以一个简单的权限控制需求为例,介绍了MyBatis基本用法,使用XML方式实现了数据库中单个表的常规操作
第3章 MyBatis注解方式的基本用法,介绍注解方式使用MyBatis。即使不通过注解方式使用,也可以通过与第2章的对比加深对MyBatis的了解
第4章 MyBatis动态SQL,通过示例详细介绍了MyBatis最强大的动态SQL功能
第5章 MyBatis代码生成器,介绍MBG的配置及用法
第6章 MyBatis高级查询,介绍了一对一、一对多和鉴别器等 高级结果映射,以及在MyBatis中使用存储过程和类型处理器的用法 第7章 MyBatis缓存配置,讲解了MyBatis缓存配置,提供了EhCache和Redis两种缓存框架的集成方法
第8章 MyBatis插件开发,介绍了MyBatis强大的扩展能力,利用插件可以很方便地在运行时改变MyBatis的行为
第9章 Spring集成MyBatis,从零开始配置搭建基本的Spring、Spring MVC、MyBatis开发环境
第10章 Spring Boot集成MyBatis,介绍了通过MyBatis官方提供的Starter集成Spring Boot的方法
第11章 MyBatis开源项目,首先介绍了git和GitHub,并对MyBatis源码作了简单介绍
附录 类型处理器,列举了常用TypeHandler,提到了对Java 8日期(JSR-310)的支持


第1章 MyBatis入门

  • MyBatis将Java方法与SQL语句关联,这与其他ORM框架将Java对象与数据库表关联起来不同
  • MyBatis是一个结果映射框架,它提供了一个映射引擎,声明式地将SQL语句的执行结果与对象树映射起来。它的底层使用JDBC执行SQL,获得查询结果集ResultSet后,根据resultType(或resultMap)的配置将结果映射为指定类型(或其集合),返回查询结果
  • MyBatis支持声明式数据缓存,仅需将一条SQL语句标记为“可缓存”即可;提供了默认情况下基于Java HashMap的缓存实现,并提供了API供其他缓存实现使用
  • MyBatis日志的最低级别是TRACE,在这个日志级别下,它会输出执行SQL过程中的详细信息,这个级别特别适合在开发时使用
  • 最后一定不要忘记关闭sqlSession,否则可能会因为连接没有关闭导致数据库连接数过多造成系统崩溃
  • 从示例代码的日志输出可以看出,SQL、参数、结果数都是DEBUG级别,具体的查询结果列和数据都是TRACE级别
  • 本章示例中所有配置文件(mybatis-config.xml/log4j.property/XXXMapper.xml等)都可以参照官网(毕竟作者也不是神,不能无中生有自己编一个出来吧,何况还有一堆模板式的xml头)

第2章 MyBatis XML方式的基本用法

本章设定了一个采用基于角色的访问控制(RBAC,Role-Based Access Control)方式的简单权限控制需求。同时,这一需求贯穿全书示例,其中涉及单表操作、一对一、一对多、多表关联嵌套查询等情形

  • MyBatis默认是遵循“下划线转驼峰”命名方式的,所以在创建实体类时一般都按照这种方式进行创建
  • 特别注意,在实体类中不要使用基本类型,而要使用与之对应的引用类型(例如动态SQL需要进行age != null的判断,而int类型的age属性不存在为null的情形)
  • MyBatis的真正强大之处在于它的映射语句,也因此映射器的XML文件就显得相对简单
  • MyBatis 3.0相比2.0版本的一个最大变化,就是支持使用接口来调用方法:
    • MyBatis使用Java的动态代理可以直接通过接口来调用相应的方法,不需要提供接口的实现类,更不需要在实现类中使用SqlSession以通过命名空间间接调用
    • 当有多个参数的时候,通过参数注解@Param设置参数的名字省去了手动构造Map参数的过程
  • mybatis-config.xml配置文件中的mappers元素配置有以下两种方式
    <mappers>
        <mapper resource="tk/mybatis/simple/mapper/UserMapper.xml"/>
        <mapper resource="tk/mybatis/simple/mapper/RoleMapper.xml"/>
        <mapper resource="tk/mybatis/simple/mapper/UserRoleMapper.xml"/>
    </mappers>
    
    这种配置方式需要将所有映射文件一一列举出来,如果新增映射文件还需要注意在此进行配置,操作起来比较麻烦。以下方式可以避免这一问题
    <mappers>
        <package name="tk.mybatis.simple.mapper"/>
    </mappers>
    

select用法

  • 接口和XML是通过将namespace的值设置为接口的全限定名称来进行关联的,接口中的方法通过XML中标签的id属性值关联;接口方法是可以重载的,而XML中的id值不能重复,因此接口中所有同名方法对应着XML中的同一个id的方法

  • 书中提到#{},所以在此稍微扩展一下,#{}${}作比较

    • 含义:#{}为占位符,${}为拼接符
    • 用法:#{}为参数占位符(即SQL预编译),${}为字符串替换(即字符串拼接)
    • 执行流程 #{}:动态解析 --> 预编译 --> 运行;${}: 动态解析 --> 编译 -->运行
    • 变量替换 #{}:变量替换是在DBMS(数据库管理系统)中,会对对应的变量自动加上单引号;${}:变量替换是在DBMS外,不会对对应的变量加上单引号
    • SQL注入#{}可以防止SQL注入,${}不可以防止SQL注入
    • 使用技巧与建议
      1. 不论是单个参数还是多个参数,一律建议使用@Param("")
      2. 能用#{}的地方尽量使用#{},减少${}
      3. 在表名作为参数时或order by时,必须使用${}
      4. 使用${}时要注意何时加或不加单引号
  • resultMap标签用于配置Java对象的属性和查询结果列的对应关系,是一种很重要的配置结果映射的方法,必须熟练掌握它的配置方法

  • 通过resultMap中配置的columnproperty可以将查询列的值映射到type对象的属性上,因此即使使用select *查询所有列时MyBatis也可以将结果正确映射到Java对象上;但是考虑到性能,通常都会指定查询列,很少使用*代替所有列

  • resultMap中的idresult标签并没有什么不同,它们的属性值是通过setter方法注入的,不同之处在于,id代表的是主键(或唯一值)的字段(可以有多个),标记结果作为id(唯一值)可以帮助提高整体性能

  • id/result标签的javaType属性,如果映射到一个Java Bean,MyBatis通常可以自动判断属性的类型;如果映射到HashMap则需要明确地指定javaType属性

  • id/result标签的jdbcType属性表示列对应的数据库类型。JDBC类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。这是JDBC jdbcType的需要,而不是MyBatis的需要

  • 可以通过在resultMap中配置property属性和column属性的映射,或者在SQL中为所有列名和属性名不一致的列设置别名这两种方式实现将查询列映射到对象属性的目的;property属性或别名要和对象中属性的名字相同,但是实际匹配时,MyBatis会先将两者都转换为大写形式,然后再判断是否相同

  • MyBatis可以通过配置全局属性mapUnderscoreToCamelCase为true,自动将以下划线方式命名的数据库列映射到Java对象的驼峰式命名属性中。这个属性默认为false

    <!--mybatis-config.xml-->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    
  • 当执行的SQL返回多个结果时,必须使用List<T>T[]作为返回值,如果使用T,就会抛出TooManyResultsException异常

insert用法

与select相比,insert要简单很多。只有让它返回主键值时,由于不同数据库的主键生成方式不同,会有一些复杂

  • 标签的flushCache属性,默认值为true,任何时候只要语句被调用,都会清空一级缓存和二级缓存
  • 标签的statementType属性,标记操作SQL的对象
    • 可选值有STATEMENTPREPARED(默认值)、CALLABLE
    • STATEMENT:使用Statement对象直接操作SQL,SQL就是直接进行的字符串拼接,不进行预编译-->获取数据
    • PREPARED:使用PreparedStatement对象预处理参数-->进行预编译-->获取数据
    • CALLABLE:使用CallableStatement 执行存储过程
    • 如果值为STATEMENT,那么SQL就是直接进行的字符串拼接,需要为字符串加上引号;如果值为PREPARED,使用的是参数替换,也就是索引占位符,我们的#会转换为?再设置对应的参数的值
  • 为了防止类型错误,对于一些特殊的数据类型(如日期、时间、二进制),建议指定具体的jdbcType值,如
    • BLOB对应的Java类型是ByteArrayInputStream,就是二进制数据流
    • 由于数据库区分date、time、datetime类型,但是Java中一般都使用java.util.Date类型,因此为了保证数据类型的正确,需要手动指定日期类型,date、time、datetime对应的JDBC类型分别为DATETIMETIMESTAMP;数据库的datetime类型可以存储DATE(时间部分默认为00:00:00)和TIMESTAMP这两种类型的时间,不能存储TIME类型的时间
  • 默认的sqlSessionFactory.openSession()是不自动提交的,因此不手动执行commit就不会提交到数据库

从insert语句获得主键值的方法

  • 使用JDBC方式返回主键自增的值 当useGeneratedKeys设置为true后,MyBatis会使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键,获得主键值后将其赋值给keyProperty配置的id属性
    <insert id="insertXXX" useGeneratedKeys="true" keyProperty="id">
        insert into tbl_user(
            user_name, user_password, user_email, create_time) 
        values(
            #{userName}, #{userPassword}, #{userEmail}, #{createTime, jdbcType=TIMESTAMP})
        )
    </insert>
    
    注意,由于要使用数据库返回的主键值,所以SQL上下两部分的列中去掉了id列和对应的#{id}属性
  • 使用selectKey返回主键的值 第一种回写主键的方法只适用于支持主键自增的数据库。有些数据库(如Oracle)不提供主键自增的功能,而是使用序列得到一个值,然后将这个值赋值给id,再将数据插入数据库。这种方式不仅适用于不提供主键自增功能的数据库,也适用于提供主键自增功能的数据库。 selectKey中的内容就是一个独立的SQL语句,以下是MySQL示例:
    <insert id="insertYYY">
        <!--insert语句不包含id列和#{id}-->
    
        <!--order属性的设置和使用的数据库有关,对于MySQL设置为AFTER,因为当前记录的主键值再insert语句执行成功后才能获取到-->
        <selectKey keyColumn="id" resultType="long" keyProperty="id" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
    </insert>
    
    以下是Oracle数据库的示例,注意二者的不同:
    <insert id="insertYYY">
        <!--selectKey元素的位置不会影响其中语句与insert语句的先后关系,这么写仅仅是为了符合实际的执行顺序,看起来更直观而已-->
        <selectKey keyColumn="id" resultType="long" keyProperty="id" order="BEFORE">
            SELECT SEQ_ID.nextval from dual
            <!--Sql Server使用SELECT SCOPE_IDENTITY() -->
        </selectKey>
    
        <!--insert语句必须包含id列和#{id},因为执行selectKey中的语句后id就有值了,需要把这个序列值作为主键值插入到数据库中-->
    </insert>
    

简单的update和delete用法并没有什么特殊之处,这里略去不表

多个接口参数的用法

实际应用中经常会遇到使用多个参数的清空,可以采用以下方式:

  • 将多个参数合并到一个Java Bean中 这种方法用起来方便,但不适合所有情况(如不方便为了两三个参数去创建新的Java Bean类)
  • 使用Map类型作为参数 这种方式还需要自己手动创建Map以及对参数进行赋值,其实并不简洁
  • 使用@Param注解
    • 如果接口方法中多个参数没有@Param进行注解,MyBatis会抛出org.apache.ibatis.binding.BindingException
    • 给参数配置@Param注解后,MyBatis就会自动将参数封装成Map类型,@Param注解值会作为Map中的key,因此在SQL部分就可以通过配置的注解值来使用参数
    • 当只有一个参数(基本类型或拥有TypeHandler配置的类型)时,为什么就可以不使用注解呢?在这种情况下(除集合、数组外,它们会在动态SQL部分介绍),MyBatis直接把这个唯一的参数值拿来使用,而并不关心这个参数叫什么名字

Mapper接口动态代理实现原理

为什么Mapper接口没有实现类却能被正常调用呢?MyBatis在Mapper接口上使用了动态代理的一种非常规用法,它和常规代理的不同之处在于,这里没有对某个具体类进行代理,而是通过代理转化成了对其他代码的调用。熟悉它不仅有利于理解MyBatis接口和XML的关系,还能开阔思路。欲知更多详情,源码之前,了无秘密

// Mapper接口
public interface UserMapper {
    List<User> selectAll();
}

// 使用Java动态代理方式创建一个代理类
public class MyMapperProxy<T> implements InvocationHandler {
    private Class<T> mapperInterface;
    private SqlSession sqlSession;

    public MyMapperProxy(Class<T> mapperInterface, SqlSewwion sqlSession) {
        this.mapperInterface = mapperInterface;
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 由于方法参数和返回值存在多种情况,MyBatis内部实现要复杂得多
        // 这里只演示最简单的无参数且返回值无需处理的情况
        List<T> list = sqlSession.selectList(
            mapperInterface.getCanonicalName() + "." + method.getName());
        return list;
    }
}

public class MyMapperTest {
    @Test
    public void test() {
        SqlSession sqlSession = ...;
        MyMapperProxy userMapperProxy = new MyMapperProxy(UserMapper.class, sqlSession);
        UserMapper userMapper = Proxy.newProxyInstance(
            Thredad.currentThread().getContextClassLoader(),
            new Class[] { UserMapper.class },
            userMapperProxy);
        List<User> user = userMapper.selectAll();
    }
}

从代理类中可以看到,当调用一个接口的方法时,会先通过接口的全限定名和当前调用方法名组合得到方法id,这也就是XML中namespace和具体方法id的组合,通过这种方式可以将接口和XML文件中的方法关联起来。


第3章 MyBatis注解方式的基本用法

  • MyBatis注解方式就是在接口方法上添加需要的注解(@Select/@Insert/@Update/@Delete等),并写上相应的SQL语句。MyBatis的注解方式不是主流,一般情况下不建议使用(因此本章篇幅也相对较短)
  • 注解方式的优点是对于需求比较简单的系统效率较高,缺点是当SQL有变化时需要重新编译代码,不方便维护
  • @Insert注解可能存在回写主键的问题,稍微复杂一些
    // 返回自增主键
    @Insert({"insert into xxx"})
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(Role role);
    
    // 返回非自增主键
    @Insert({"insert into xxx"})
    @SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", resultType = Long.class, before = false)
    int insert(Role role);
    
  • MyBatis还提供了4种Provider注解(@SelectProvider/@InsertProvider/@UpdateProvider/@DeleteProvider),同样可以实现CRUD操作
    public interface UserMapper {
        @SelectProvider(type = UserProvider.class, method = "selectById")
        User selectById(Long id);
    }
    
    public class UserProvider {
        public String selectById(final Long id) {
            // 一般直接返回SQL字符串即可
            // return "select id, user_name, user_email from tbl_user where id = #{id}";
    
            // 也可以使用new SQL(){...}方式,在SQL较长或需要拼接时推荐使用
            return new SQL() {
                SELECT("id, user_name, user_email");
                FROM("tbl_user");
                WHERE("id = #{id}");
            }.toString();
        }
    }
    

第4章 MyBatis动态SQL

  • 手动拼接SQL语句时经常需要仔细处理空格、逗号、and/or等细节问题,不仅麻烦而且容易出错。动态SQL是MyBatis最强大的特性之一,可以帮我们避免这种种麻烦
  • MyBatis 3采用了OGNL(Object-Graph Navigation Language)表达式语言,消除了之前版本的许多其他标签。当前支持的动态SQL标签包括:ifchoosewhenotherwise)、trimwhereset)、foreachbind
  • 通过动态SQL可以避免在Java代码中处理繁琐的业务逻辑。将大量的判断写入MyBatis的映射层,可以极大程度上提高代码的灵活性。当有一般的业务逻辑改动时,通常只需要在映射层通过动态SQL即可实现

if用法

适用场景

  • 在WHERE语句中,通过判断参数值来决定是否使用某个查询条件
  • 在UPDATE语句中,判断是否更新某个字段
  • 在INSERT语句中,用来判断是否插入某个字段的值

在WHERE条件中使用if

<!--UserMapper.xml-->
<!-- 必填属性test的值是一个符合OGNL要求的判断表达式 -->
<select id="selectByUser" resultType="tk.mybatis.sample.model.User">
    select user_name userName, user_email userEmail from tbl_user where 1 = 1
    <if test="userName != '' and userName != null">
    and user_name like concat('%', #{userName}, '%')
    </if>
    <if test = "userEmail != '' and userEmail != null">
    and user_email = #{userEmail}
    </if>
</select>

上述XML中有两处需要注意:

  1. where 1 = 1。由于这里两个条件都是动态的,如果两个if判断都不满足,最后生成的SQL就会以where结束。这种写法并不美观,where标签可以替代这种写法
  2. 注意条件中的and/or。这里的and/or需要手动添加,这样拼接到where 1 = 1后面时得到的仍是合法SQL语句。也正是由于有了默认的1 = 1这个条件,才不需要去另外判断第一个动态条件之前是否需要加上and/or

在UPDATE更新列中使用if

通过if标签可以实现只更新有变化的字段这种动态列更新

<!--UserMapper.xml-->
<!-- 一般情况下,MyBatis中选择性更新的方法名以Selective作为后缀 -->

<update id="updateByIdSelective">
    update tbl_user set
    <if test="userName != '' and userName != null">
    user_name = #{userName},
    </if>
    <if test="userEmail != '' and userEmail != null">
    user_email = #{userEmail},
    </if>
    id = #{id}
    where id = #{id}
</update>

与select语句一样,必须确保最终生成的SQL语句没有语法错误。上述XML中有两处需要注意:

  1. 每个if元素里面SQL语句后面的逗号
  2. where语句之前的id = #{id} 如果没有了这句,最终生成的SQL语句就有可能不合法:update tbl_user set where id = #{id}(当全部if判断都不满足时)或update tbl_user set user_name = #{userName}, where id = #{id}(当仅有第一个if判断满足时)

除了示例中的这种方式外,还可以通过调整XML文件中的SQL来确保最终的SQL语句的正确性,或通过where和set标签来解决这些问题

在INSERT动态插入列中使用if

在数据库表中插入数据时,如果某一列的参数值不为空就使用传入的值,否则就使用数据库中的默认值。使用if就可以实现这种动态插入的功能

<!--UserMapper.xml-->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
    insert into tbl_user(
        user_name, 
        <if test="userEmail != '' and userEmail != null">
        user_email,
        </if>
        create_time
    )
    values(
        #{userName},
        <if test="userEmail != '' and userEmail != null">
        #{userEmail},
        </if>
        #{createTime, jdbcType=TIMESTAMP}
    )
</insert>

需要注意之处在于,若在列的部分增加if条件,则values的部分也要增加爱相同的if条件,即必须保证上下对应、完全匹配

choose用法

if标签提供了基本的条件判断,但它无法实现if...else或if...else...的逻辑。要实现这样的逻辑,需要用到choose when otherwise标签

<!--UserMapper.xml-->
<select id="selectByIdOrUserName" resultType="tk.mybatis.sample.model.User">
    select id, user_name userName, user_email userEmail, create_time createTime
    from tbl_user
    where 1 = 1
    <choose>
        <when test="id != null">
        and id = #{id}
        </when>
        <when test="userName != '' and userName != null">
        and user_name = #{userName}
        </when>
        <otherwise>
        and 1 = 2
        </otherwise>
    </choose>
</select>

使用choose when otherwise标签时要注意逻辑严密,也就是考虑各种情况下生成的SQL语句是否符合业务需求

where、set、trim用法

这3个标签解决了类似的问题,并且where和set都属于trim的一种具体用法

where用法

where标签的作用:如果该标签包含的元素中有返回值,就插入一个WHERE;如果WHERE后面的字符串是以AND/OR开头的,就将它们删除。 我的理解:上面这段话就是在说,where标签的作用在于构建合法的WHERE子句(包括WHERE子句不存在的情况)

<!--UserMapper.xml-->
<select id="selectByUser" resultType="tk.mybatis.sample.model.User">
    select id, user_name userName, user_email userEmail
    from tbl_user
    <where>
       <if test="userName != '' and userName != null">
        and user_name like concat('%', #{userName}, '%')
        </if>
        <if test = "userEmail != '' and userEmail != null">
        and user_email = #{userEmail}
        </if> 
    </where>   
</select>

与之前那种通过where 1 = 1曲线救国的方式相比,上例生成的SQL更干净直观

set用法

set标签的作用:如果该标签包含的元素中有返回值,就插入一个set;如果set后面的字符串是以逗号结尾的就将这个逗号删除。也就是说,set标签解决了逗号的问题,但并没有解决全部问题(具体说就是set元素中内容为空的情况下,UPDATE语句因缺少SET子句仍会出现SQL错误)

<!--UserMapper.xml-->
<update id="updateByIdSelective">
    update tbl_user
    <set>
       <if test="userName != '' and userName != null">
        user_name = #{userName},
        </if>
        <if test = "userEmail != '' and userEmail != null">
        user_email = #{userEmail},
        </if> 
        id = #{id},
    </set> 
    where id = #{id}
</update>

trim用法

where和set标签的功能都可以用trim标签来实现,并且它们在底层就是通过TrimSqlNode实现的

  • where标签对应的trim的实现如下
    <!--这里的AND和OR后面的空格不能省略,以免匹配到andess、orders等无关单词-->
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
    ...
    </trim>
    
  • set标签对应的trim实现如下
    <trim prefix="SET" suffixOverrides=",">
    ...
    </trim>
    
  • trim标签的属性含义如下
    • prefix:当trim元素内包含内容时,会给内容增加prefix指定的前缀
    • suffix:当trim元素包含内容时,会给内容增加suffix指定的后缀
    • prefixOverrides:当trim元素内包含内容时,会把内容中匹配的前缀字符串去掉
    • suffixOverrides:当trim元素内包含内容时,会把内容中匹配的后缀字符串去掉
  • 这下可以解释为何set标签只解决了后缀逗号问题,而未解决空SET子句问题;以及为何where标签可以同时解决了前缀AND/OR和空子句问题

foreach用法

foreach可以对Map或实现了Iterable接口的对象进行遍历(数组在处理中会转换为List对象),这两种类型在遍历循环时情况不一样

foreach实现in集合

SQL语句有时会使用IN关键字,例如id in (1, 2, 3)。使用${}的方式也能达到目的,但这种写法不能防止SQL注入,这时#{}配合foreach标签使用可以满足需求

<!--UserMapper.xml-->
<select id="selectByIdList" resultType="tk.mybatis.sample.model.User">
    select * from tbl_user
    where id in
    <foreach collection="list" open="(" close=")" separator="," item="id" index="i">
        #{id}
    </foreach>
</select>
  • foreach各属性解释如下:
    • collection:必填,值为要迭代循环的属性名。这个属性值的情况有很多
    • item:变量名,值为从迭代对象中取出的每一个值
    • index:索引的属性名,在集合数组情况下值为当前索引值;当迭代循环的对象是Map类型时,这个值为Map的key
    • open:整个循环内容开头的字符串
    • close:整个循环内容结尾的字符串
    • separator:每次循环的分隔符
  • collection属性值的几种情况
    1. 单参数(数组或集合类型):使用默认的名字,即参数类型是List时值为"list",参数类型是数组时值为"array"
    2. 单个或多个参数(通过@Param注解指定了参数名):collection值指定为"注解指定的参数名"
    3. 参数是Map类型:与情形2类似,值指定为"Map中对应的key"
    4. 参数是一个对象:值指定为对象的属性名;使用“属性.属性”的方式可以指定深层的属性值

foreach实现动态UPDATE

当参数是Map类型时,foreach标签的index属性值对应的不是索引值,而是Map中的key。利用这个key可以实现动态UPDATE

<!--UserMapper.xml-->
<update id="updateByMap">
    update tbl_user
    set
    <foreach collection="_parameter" item="val" index="key" separator=",">
        ${key} = ${val}
    </foreach>
</update>

这里假设没有通过@Param注解指定,MyBatis在内部的上下文中会默认使用_parameter表示该参数,所以XML中使用了collection="_parameter"

bind用法

bind标签可以使用OGNL表达式创建一个变量并将其绑定到上下文中

<!--由于不同数据库之间的语法差异,有些SQL语句在更换数据库时可能会遇到兼容问题,需要修改-->
<if test="userName != '' and userName != null">
    and user_name like concat('%', #{userName}, '%')
</if>

<!--通过bind绑定了userNameLike变量(变量值为拼接好的字符串),避免了因更换数据库而修改SQL,也能预防SQL注入-->
<if test="userName != '' and userName != null">
    <bind name="userNameLike" value="'%' + userName + '%'"/>
    and user_name like #{userNameLike}
</if>

多数据库支持

bind标签不能解决更换数据库带来的所有问题。MyBatis基于映射语句中的databaseId属性提供了多数据库厂商支持,能根据不同的数据库执行不同的语句

<!--mybatis-config.xml-->
<!--DB_VENDOR = DatabaseMetaData.getDatabaseProductName()-->
<databaseIdProvider type="DB_VENDOR">
    <!--DB_VENDOR通常比较长,而且可能包含版本信息。这里通过property标签设置简短好记的databaseId-->
    <property name="SQL Server" value="sqlserver"/>
    <property name="Oracle" value="oracle"/>
    <property name="MySQL" value="mysql"/>
    <property name="HSQL" value="hsqldb"/>
    <property name="H2" value="h2"/>
</databaseIdProvider>

getDatabaseProductName()返回的字符串包含property中name部分的值即可匹配,因此“Microsoft SQL Server”能够匹配第一条,从而databaseId被赋值为"sqlserver"

datataseId属性如何使用:

<select id="selectByUser" databaseId="mysql" resultType="tk.mybatis.sample.model.User">
    select * from tbl_user where user_name like concat('%', #{userName}, '%')
</select>
<!--使用相同的id-->
<select id="selectByUser" databaseId="oracle" resultType="tk.mybatis.sample.model.User">
    select * from tbl_user where user_name like '%' || #{userName} || '%'
</select>

数据库的更换可能只会引起某个SQL语句的部分不同,上面这种写法产生大量重复的SQL。可以使用if标签配合默认的上下文中的_databaseId参数去实现局部适配不同数据库

<select id="selectByUser" resultType="tk.mybatis.sample.model.User">
    select * from tbl_user
    <where>
        <if test="userName != '' and userName != null">
            <if test="_databaseId == 'mysql'">
            and user_name like concat('%', #{userName}, '%')
            </if>
            <if test="_databaseId == 'oracle'">
            and user_name like '%' || #{userName} || '%'
            </if>
        </if>
        <if test="userEmail != null and userEmail != ''">
        and user_email = #{userEmail}
        </if>
    </where>
</select>

OGNL用法

在MyBatis的动态SQL和${}形式的参数中都用到了OGNL表达式(Object-Graph Navigation Language)。这一节对OGNL的用法作了简单介绍,笔记不写了,待用到再结合官网查阅吧


第5章 MyBatis代码生成器

本章对MyBatis Generator(MBG)作了较为全面的介绍。对MBG了解越多,越能在使用时减少人工操作,节省大量时间,从枯燥的基础方法中脱离出来 本章主要介绍工具使用,暂时不做笔记了


第6章 MyBatis高级查询

之前几章介绍了MyBatis中最常用的部分,即基本的CRUD操作以及动态SQL,它们可以满足日常大部分的需求。本章介绍了MyBatis的高级结果映射,包括数据库一对一、一对多的查询,存储过程的使用,以及对Java枚举Java 8日期的支持

高级结果映射

在面对多表关联查询需求时,我们可能要写多个方法分别查询,然后再将它们组合到一起。这种处理方式特别适合用于大型系统上,由于分库分表,这种用法可以减少表之间的关联查询,方便系统进行扩展。不过,在一般的企业应用中,使用MyBatis的高级结果映射便可以轻松处理这种一对一、一对多的关系

一对一映射

使用自动映射

一对一映射因为不需要考虑是否存在重复数据,可以直接使用MyBatis的自动映射,即通过别名让MyBatis自动将值匹配到对应字段上

public class User {
    // 其他原有字段

    private Role role;

    // setter/getter方法
}

为User类增加role属性,并修改对应的Mapper文件如下。通过这种方式,MyBatis会将查询出的一条数据自动映射到两个类中

<!--UserMapper.xml-->
<select id="selectUserAndRoleById" resultType="tk.mybatis.sample.model.User">
    select u.id, u.user_name userName, r.id "role.id", r.role_name "role.roleName"
    from tbl_user u
    inner join tbl_user_role ur on u.id = ur.user_id
    inner join tbl_role r on r.id = ur.role_id
    where u.id = #{id}
</select>
  • 这种通过一次查询将结果映射到不同对象的方式,称之为关联的嵌套结果映射
  • 使用这种方式的好处是减少数据库查询次数,减轻数据库压力;缺点是要写复杂的SQL,当嵌套结果比较复杂时容易写错。由于要在应用服务器上将结果映射到不同类上,也会增加应用服务器的压力
  • 一定会使用到嵌套结果,并且整个复杂SQL执行速度很快时(那怎么知道这是个慢查询呢?这又是另一个话题了),建议使用这种方式
使用resultMap配置映射关系
(a)关联的嵌套结果映射

改进1: 将resultType替换为resultMap

<!--UserMapper.xml-->
<resultMap id="userRoleMap" type="tk.mybatis.sample.model.User">
    <id property="id" column="id"/>
    <result property="userName" column="user_name"/>
    <result property="userEmail" column="user_email"/>
    <!--其他property/column-->
    <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>

    <!--role相关属性-->
    <!--特别注意,所有property配置都使用了"role."前缀,column配置所有可能重名的列都增加了"role_"前缀-->
    <result property="role.id" column="role_id"/>
    <result property="role.roleName" column="role_name"/>
    <result property="role.createBy" column="create_by"/>
    <result property="role.createTime" column="role_create_time" jdbcType="TIMESTAMP"/>
</resultMap>

<select id="selectUserAndRoleById2" resultMap="userRoleMap">
    select u.id, u.user_name userName, r.id role_id, r.role_name, r.create_time role_create_time
    from tbl_user u
    inner join tbl_user_role ur on u.id = ur.user_id
    inner join tbl_role r on r.id = ur.role_id
    where u.id = #{id}
</select>

与自动映射的方式对比,很显然这种方式非常烦琐,不仅没有方便使用,反而增加了更多工作量

改进2: 继承已有的resultMap映射

<!--UserMapper.xml-->
<resultMap id="userRoleMap" extends="userMap" type="tk.mybatis.sample.model.User">
    <!--继承已有的userMap,并添加role特有的配置即可-->
    <result property="role.id" column="role_id"/>
    <result property="role.roleName" column="role_name"/>
    <result property="role.createBy" column="create_by"/>
    <result property="role.createTime" column="role_create_time" jdbcType="TIMESTAMP"/>
</resultMap>

使用继承不仅使得配置更简单,而且当对主表userMap进行修改时也只需要修改一处

改进3: 使用resultMapassociation标签配置一对一映射 在resultMap中,association标签用于和一个复杂的类型进行关联,即用于一对一的关联配置

<!--UserMapper.xml-->
<resultMap id="userRoleMap" extends="userMap" type="tk.mybatis.sample.model.User">
    <association property="role" columnPrefix="role_" javaType="tk.mybatis.sample.model.Role">
        <!--不再需要手写"role."以及"role_"前缀-->
        <result property="id" column="id"/>
        <result property="roleName" column="role_name"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
    </association>
</resultMap>

<!--由于配置了columnPrefix,SQL需要作修改-->
<select id="selectUserAndRoleById3" resultMap="userRoleMap">
    <!--注意,role_role_name是role_name增加role_前缀的结果-->
    select u.id, u.user_name userName, r.id role_id, r.role_role_name, r.create_time role_create_time
    from tbl_user u
    inner join tbl_user_role ur on u.id = ur.user_id
    inner join tbl_role r on r.id = ur.role_id
    where u.id = #{id}
</select>

改进4: 将association使用resultMap属性配置成已有的resultMap映射

<!--UserMapper.xml-->
<resultMap id="userRoleMap" extends="userMap" type="tk.mybatis.sample.model.User">
    <!--resutlMap需要配置全限定名,即"namespace.id",否则MyBatis默认添加当前namespace-->
    <association 
        property="role" 
        columnPrefix="role_" 
        resultMap="tk.mybatis.sample.mapper.RoleMapper.roleMap"
    />
</resultMap>

至此,演进完成。可以看到,与最初版本相比,XML的配置已经大大简化了

(b)关联的嵌套查询

除了通过复杂的SQL查询获取结果,还可以利用简单的SQL通过多次查询转换为我们需要的结果,最后将结果组合成一个对象。这种方式与我们根据业务逻辑手动执行多次SQL的方式相像

<!--UserMapper.xml-->
<resultMap id="userRoleMapSelect" extends="userMap" type="tk.mybatis.sample.model.User">
    <association 
        property="role" 
        column="{id=role_id}" 
        select="k.mybatis.sample.mapper.RoleMapper.selectRoleById"
    />
</resultMap>

<select id="selectUserAndRoleByIdSelect" resultMap="userRoleMapSelect">
    select u.id, u.user_name, u.user_email, u.create_time, ur.role_id
    from tbl_user u
    inner join tbl_user_role ur on u.id = ur.user_id
    where u.id = #{id}
</select>

association标签常用属性如下:

  • select:另一个映射查询的id,MyBatis会额外执行这个查询获取嵌套对象的结果
  • column:列名(或别名),将主查询中列的结果作为嵌套查询的参数
  • fetchType:数据加载方式,可选值为lazy/eager(延迟加载/积极加载)。这个配置会覆盖全局的lazyLoadingEnabled

嵌套查询如下:

<!--RoleMapper.xml-->
<select id="selectRoleById" resultMap="roleMap">
    select * from tbl_role where id = #{id}
</select>

可用的参数是通过column="{id=role_id}"配置的,因此嵌套的SQL中只能使用#{id}。当需要多个参数时,可以配置多个,用逗号隔开

  • 嵌套查询要考虑的两个问题
    1. 如果查询出来并没有使用,那就白白浪费一次查询
    2. N+1问题:如果主查询得到N条结果,这N条结果要各自执行一次嵌套查询,那么共需要进行N+1次查询
  • 延迟加载可以解决上述两个问题,即配置fetchType=“lazy”
    • MyBatis的全局配置中,有个参数aggressiveLazyLoading,默认值为true,此时对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性都将按需加载
    • 由于aggressiveLazyLoading默认为true,当查询tbl_user过后并给User对象赋值时,会调用该对象其他属性的setter方法,于是触发上述规则,导致本该延迟加载的属性直接加载
  • 配置全局属性以启用延迟加载
    <!--mybatis-config.xml-->
    <settings>
        <!--其他配置-->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    
  • MyBatis延迟加载是通过动态代理实现的,当调用配置为fetchType=false的属性方法时,动态代理的操作会被触发,这些额外的操作就是通过MyBatis的SqlSession去执行嵌套SQL的
  • MyBatis提供了参数lazyLoadTriggerMethods,意在当调用配置中的方法时,加载全部的延迟加载数据。默认值为“equals, clone, hashCode, toString”(我估计是因为这些方法一般需要知晓对象的全部属性)
  • 在和某些框架集成时,SqlSession的生命周期交给了框架来管理,因此当对象超出SqlSession生命周期调用时,会由于连接关闭而抛出异常。在和Spring集成时,要确保只能在Service层调用延迟加载的属性(当结果从Service层返回至Controller层时,获取延迟加载的属性值,会由于SqlSession已经关闭而抛出异常)

一对多映射

一对多映射中,嵌套查询和延迟加载与一对一映射的情况几乎完全一样,唯一不同的只是映射结果的数量

(a)collection集合的嵌套结果映射

与一对一映射的情况相比,唯一的不同只是将association替换为collection

<!--UserMapper.xml-->
<resultMap id="userRoleListMap" extends="userMap" type="tk.mybatis.sample.model.User">
    <collection 
        property="roles" 
        columnPrefix="role_" 
        resultMap="tk.mybatis.sample.mapper.RoleMapper.roleMap"
    />
</resultMap>

当然,property值也要由"role"变为"roles",这是由User对象的结构决定的

public class User {
    // 原有属性

    private List<Role> roles;

    // getter/setter方法
}

MyBatis将多条数据库查询结果映射为一对多的Java对象的处理规则

问题: 假设查询的某一用户拥有3个角色,转换为一对多的数据结构后就变成3条结果。那么,MyBatis是如何“知道”要合并哪几条数据,使其映射为成1个用户对应3个角色这样的结果的呢?

分析如下:

  1. MyBatis是如何“知道”要合并哪些数据的? MyBatis在处理结果时,会判断结果是否相同,如果是相同的结果,则只会保留第一个结果,所以这个问题转化为:
  2. MyBatis如何判断结果是否相同? 当配置了id标签时,MyBatis只需逐条比较所有数据中id标签配置的字段值是否相同;否则,比较resultMap中配置的所有字段进行比较,只要有一个字段值不同就认为结果不相同。 为什么是这样呢?
  3. resultMap中配置id标签有什么作用? 前面刚开始接触resultMap时提到,resultMap中的idresult标签不同之处在于,id代表的是主键(或唯一值)的字段(可以有多个),标记结果作为id(唯一值)可以帮助提高整体性能。实际上,MyBatis的resultMap只用于配置结果如何映射,并不知道这个表具体如何(例如哪个字段为主键)。所以,id标签的唯一作用就是,在嵌套的映射配置时判断数据是否相同
  4. 如果将id改为result,查询结果会有什么变化? 查询结果相同,但是改为result后MyBatis不知道如何对查询结果作合并,只能逐一比较所有字段是否相同。假设字段数为M,查询结果有N条,则需要进行M*N次比较,相比配置id时的N次比较,效率相差很多。因此,在配置嵌套结果查询时,配置id标签可以提高处理效率

上面的例子只有一层嵌套,相对比较简单。如果要配置一个相当复杂的映射(如多层嵌套),一定要从基础映射开始配置,没增加一些配置就进行对应的测试,在循序渐进的过程中更容易发现和解决问题

(b)collection集合的嵌套查询

结合一对多嵌套结果映射时collection的用法以及一对一映射中的association嵌套查询的用法,这一节内容会变得比较容易

鉴别器映射

鉴别器(discriminator)映射是一种很少使用的方式,在使用前一定要完全掌握,没有把握的情况下尽可能避免使用!它很像Java语言中的switch语句,如下例所示

<!--RoleMapper.xml-->
<resultMap id="rolePrivilegeListMapChoose" type="tk.mybatis.sample.model.Role">
    <discriminator column="enabled" javaType="int">
        <case value="1" resultMap="rolePrivilegeListMapSelect"/>
        <case value="0" resultMap="roleMap"/>
    </discriminator>
</resultMap>

上面的配置表示,当角色的属性enabled值为1时,使用rolePrivilegeListMapSelect映射(一对多)以获取到该角色下详细的权限信息;enabled值为0时,使用roleMap映射,只获取角色的基本信息,而不能获取角色的权限信息

存储过程

存储过程我平时使用得比较少,了解得也有限,这部分就只是通读了一遍,笔记只摘抄部分零碎知识点

  • 存储过程在数据库中比较常见,虽然大多数存储过程的调用比较复杂,但是使用MyBatis调用时,用法都一样
  • 在使用select标签调用存储过程时,由于存储过程方式不支持MyBatis的二级缓存,因此为了避免缓存配置出错,直接将select标签的useCache属性设置为false
  • 在MyBatis映射的Java类中,一般不推荐使用基本类型(见第2章笔记)。当某个字段的数据库类型为BLOB时,对应的Java类型通常都是写成byte[]字节数组的形式,因为字节数组不存在默认值的问题,所以不影响一般使用。要特别注意的是,XML文件中设置javaType时需要设为_byte[]。MyBatis中byte对应的是Byte类型,_byte对应的时基本类型
  • 对于上面这一条,我在官网文档找到了出处。采取这一特殊的命名风格,主要是为了应对原始类型的命名重复。常见的例子还有map对应Map以及hashmap对应HashMap

使用枚举

假设有如下需求:在tbl_user表中有一个enabled字段,只有两个可选值,0为禁用,1为启用。那么在对应的Java类中能否使用枚举类型来表示这一字段呢?

新增Enabled枚举类如下

package tk.mybatis.sample.type;
public enum Enabled {
    disbled,
    enabled;
}

User中enabled字段类型由Integer修改为Enabled

private Enabled enabled;
// getter/setter略
  • 数据库中不存在一个和Enabled枚举对应的数据类型,因此在和数据库交互时不能直接使用枚举类型
  • MyBatis在处理Java类型和数据库类型时,使用TypeHanlder对这两者进行转换。MyBatis在启动时会加载所有的JDBC对应的类型处理器
  • MyBatis内置了两个枚举转换器org.apache.ibatis.type.EnumTypeHandlerorg.apache.ibatis.type.EnumOrdinalTypeHandlerEnumTypeHandler是默认的枚举转换器,它将枚举实例转换为实例名称的字符串(这个例子中就是"disabled"或"enabled"),这并不符合要求;EnumOrdinalTypeHandler使用枚举实例的ordinal属性作为取值(这个例子中时0或1),刚好满足需求
  • 启用类型处理器
    <!--mybatis-config.xml-->
    <typeHandlers>
        <typeHandler
            javaType="tk.mybatis.sample.type.Enabled"
            handler="rg.apache.ibatis.type.EnumOrdinalTypeHandler"
        />
    </typeHandlers>
    
  • 有时数据库对应的值既不是枚举的字面值,也不是索引值,这种情况下就需要自己来实现类型处理器。MyBatis提供了org.apache.ibatis.type.BaseTypeHandler类用于我们扩展类型处理器(根据如何在MyBatis中优雅地使用枚举这篇文章);书中介绍的例子是实现org.apache.ibatis.type.TypeHandler接口
  • 如果需要用到复杂的类型处理,可以参考MyBatis项目中org.apache.ibatis.type包下的各种类型处理器的实现

对Java 8日期(JSR-310)的支持

  • MyBatis从3.4.0版本开始增加了对Java 8日期的支持,只需在Maven中添加org.mybatis.mybatis-typehandlers-jsr310依赖。其实,我在官网上看到最新的说法是,从3.4.5开始,MyBatis默认支持JSR-310,应该就是说不用手动添加上面这个依赖了。当然了,这是大势所趋,毕竟Java 8出来多久了。。。
  • 要在早期版本中启用支持,需要在mybatis-config.xml文件中添加配置(没有在官网上找到配置来源,假如不幸碰到,姑且从作者这里抄吧,或者Google大法。。。)

第7章 MyBatis缓存配置

一般提到MyBatis缓存时,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制,因此很少会提到

一级缓存

  • MyBatis的一级缓存是和SqlSession绑定的,只存在于SqlSession的生命周期中。在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。当再次以相同参数执行同一方法时则会返回缓存的对象
  • 如果不想让某一方法使用一级缓存,可以为该方法添加flushCache="true"。但是,由于它清空了一级缓存,会影响当前SqlSession中所有缓存的查询,在以读为主的场景下要避免这种做法
  • 任何的INSERT/UPDATE/DELEETE操作都会清空一级缓存

二级缓存

  • 二级缓存全局开关,默认值为true,即启用状态,因此一般不必配置

    <!--mybatis-config.xml-->
    <settings>
        <!--其他配置-->
        <setting name="cacheEnabled" value="true">
    </settings>
    
  • MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件中,或Mapper.java接口中

    <mapper namespace="tk.mybatis.sample.mapper.UserMapper">
        <!--注意,要保证全局配置cacheEnabled启用的情况下才能生效-->
        <cache/>
        <!--其他配置-->
    </mapper>
    
  • 默认的二级缓存行为如下:

    • 映射文件中所有SELECT语句将会被缓存
    • 映射文件中所有INSERT/UPDATE/DELETE语句会刷新缓存
    • 缓存会使用LRU算法来回收
    • 没有刷新间隔,缓存仅在调用语句时刷新
    • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用
    • 缓存默认是可读写(read/write)的

    默认行为可以通过<cache/>标签的属性来修改,如下

    <cache
        eviction="FIFO"
        flushInterval="60000"
        size="512"
        readOnly="false"
    />
    

使用二级缓存

  • 只读缓存具有性能优势;可读写缓存安全,因此默认配置为可读写缓存 MyBatis使用序列化缓存(org.apache.ibatis.cache.decoratros.SerializedCache)来实现可读写缓存,并通过序列化和反序列化来保证通过缓存获取数据是得到的是一个新的实例;对于只读缓存,MyBatis从Map中获取缓存,得到的对象是同一实例。
  • SerializedCache要求所有被序列化的对象必须实现java.io.Serializable接口

集成其他缓存框架

MyBatis默认提供的缓存实现是基于Map实现的内存缓存。还可以选择EhCacheRedis等工具来保存MyBatis的二级缓存数据

EhCache

  • EhCache是一个纯粹的Java进程内的缓存框架,主要特性如下。MyBatis提供了EhCache的MyBatis二级缓存实现:ehcache-cache项目
    • 快速、简单
    • 多种缓存策略
    • 缓存数据有内存和磁盘两级
    • 缓存数据会在虚拟机重启过程中写入磁盘
    • 可以通过RMI、可插入API等方式进行分布式缓存
    • 具有缓存和缓存管理器的侦听接口
    • 支持多缓存管理器实例以及一个实例的多个缓存区域
  • 集成EhCache
    • 添加Maven依赖“mybatis-ehcache”
    • 配置EhCache:src/main/resources/ehcache.xml。详细配置参考EhCache文档。配置中两个重点属性copyOnReadcopyOnWrite,会对二级缓存的使用产生很大影响 copyOnRead的含义是,判断从缓存中读取数据时是返回对象的引用(false)还是对象的拷贝(true),默认为false copyOnWrite的含义是,判断写入缓存时是直接缓存对象的引用还是对象的拷贝,默认为false
      如果想使用可读写缓存,就需要将这两个属性配置为true;如果使用只读缓存,可以不配置这两个属性,也就是使用默认值false
    • 修改映射文件中缓存的配置
      <!--RoleMapper.xml-->
      <mapper namespace="tk.mybatis.sample.mapper.RoleMapper">
          <cache type="org.mybatis.caches.ehcache.EhcacheCache">
          <!--其他配置-->
      </mapper>
      
      通过设置type属性便可以使用EhCache缓存了,这时cache的其他属性都不会起任何作用,针对缓存的配置都在ehcache.xml文件中进行

集成Redis缓存

  • Redis是一个高性能的key-value数据库,MyBatis提供了Redis的MyBatis二级缓存实现,项目名为redis-cache
  • 集成Redis
    • 添加Maven依赖“mybatis-redis”。PS:特意去Maven Repository查了一下,依然是beta版本。。。
    • 配置Redis:src/main/resources/redis.properties
      host=localhost
      port=6379
      connectionTimeout=5000
      soTimeout=5000
      password=
      database=0
      clientName=
      
    • 修改映射文件中缓存的配置
      <!--RoleMapper.xml-->
      <mapper namespace="tk.mybatis.sample.mapper.RoleMapper">
          <cache type="org.mybatis.caches.redis.RedisCache">
          <!--其他配置-->
      </mapper>
      
      配置依然很简单,需要注意的是:RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此需要保证被缓存的对象实现Serializable接口

脏数据的产生和避免

二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但如果使用不当,很容易产生脏数据

  • 脏数据的产生
    • MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响
    • 关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。而涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据
  • 避免脏数据
    • 使用参照缓存,当某几个表可以作为一个业务整体时,通常是让几个关联的ER表同时使用同一个二级缓存
      <mapper namespace="tk.mybatis.sample.mapper.UserMapper">
          <cache-ref namespace="tk.mybatis.sample.mapper.RoleMapper"/>
          <!--其他配置-->
      </mapper>
      
    • 虽然这样可以解决脏数据的问题,但是并非所有关联查询都可以这么解决。如果有几十个甚至所有表都以不同的关联关系存在于各自的映射文件中,使用参照缓存显然没有意义

二级缓存适用场景

  • 以查询为主的应用中,只有尽可能少的增、删、改操作
  • 绝大多数以单表操作存在时
  • 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置
  • 除了以上推荐使用的情况,如果脏读对系统没有影响,也可以使用

在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存来代替二级缓存


第8章 MyBatis插件开发

MyBatis允许在已映射语句执行过程中的某一个点进行拦截调用。本章对MyBatis拦截器中可以拦截的每个对象的每个方法都进行了简单的介绍。 目前我对插件开发兴趣不大(主要是功力不够,实力不允许。。。),这一章就不写笔记了


第9章 Spring集成MyBatis

Spring集成MyBatis这个话题,网上的资料可以用汗牛充栋来形容了,因此关于本章的笔记主要是散落的知识点、细节摘要

  • Spring是为了解决企业应用开发的复杂性而创建的轻量级框架。Spring 3.0在MyBatis 3.0官方发布前就已经结束了,Spring开发团队不想发布一个基于非发布版MyBatis的整合支持,因此二者的集成并没有Spring官方的支持
  • Mybatis官方提供了MyBatis-Spring项目来实现对Spring的整合
  • 由于项目中可能会直接用到FilterServletRequest等接口,所以在编译项目时,必须提供servlet-api和jsp-api依赖。通常Web容器都会自带这两个依赖的jar包,为了避免jar包重复引起错误,需要将这两个依赖的scope配置为provided
  • 集成Spring时需要添加很多Spring组件的依赖,spring-framework-bom是Spring的一个项目清单文件,在pom.xml中添加该清单文件后,Spring组件的版本由spring-framework-bom统一管理,不仅可以避免因使用不同版本的组件导致意外情况发生,还可以很方便地升级Spring的版本。可见的结果就是,在使用Spring依赖时就不需要再配置每个依赖的版本号了
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>4.3.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
  • <context:component-scan base-package="tk.mybatis.*.service.impl"/>用于配置Spring自动扫描类,通过base-package属性来设置要扫描的包名。包名支持Ant通配符,包名中的*匹配0或者任意数量的字符
  • 通过自动扫描Mapper和自动注入(@Autowired)可以更方便地使用MyBatis

第10章 Spring Boot集成MyBatis

  • Spring Boot旨在帮助开发者更容易创建基于Spring的应用程序和服务,让Java开发也能实现Ruby on Rails那样的生产效率。Spring Boot项目为Spring生态系统提供了一种固定的、约定优于配置风格的框架
  • Spring Boot具有如下特性:
    • 创建独立的Spring应用程序,为开发者提供更快的入门体验
    • 尽可能地自动配置,开箱即用
    • 没有代码生成,也无须XML配置,同时也可以修改默认值来满足特定的需求
    • 提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标、健康检测、外部配置等
    • 并不是对Spring功能上的增强,而是提供了一种快速使用Spring的方式
  • MyBatis官方为了方便Spring Boot集成MyBatis,专门提供了一个符合Spring Boot规范的starter项目mybatis-spring-boot-starter符合Spring Boot规范的starter依赖都会按照默认的配置方式在系统启动时自动配置好,因此不用对MyBatis进行任何额外的配置,MyBatis就已经集成好了
  • Mapper接口添加了@Mapperorg.apache.ibatis.annotations.Mapper)注解之后,Spring启动时会自动扫描该接口,这样就可以在需要使用时直接注入Mapper了

MyBatis Starter配置介绍

  • MyBatis在不同用法中的配置方式不同,但是所有可配置的属性含义和作用都是相同的。由于Spring Boot方式有特殊的配置规范,所以这里特别介绍
  • MyBatis Starter提供的所有可配置的属性都在org.mybatis.spring.boot.autoconfigure.MyBatisProperties类中
  • Spring Boot可以通过@ConfigurationProperties注解自动将配置文件中的属性组装到对象上,这个注解一般都需要配置与属性匹配的前缀,如“mybatis”,于是对MyBatis的配置都以“mybatis.”作为前缀
  • 属性类中的字段如果时驼峰形式的,在配置文件中进行配置时建议改成横杠(-)和小写字母连接的形式。因为虽然Spring Boot仍然能正确匹配驼峰形式的属性,但是支持Spring Boot的IDE在自动提示时会使用标准的形式
  • MyBatisProperties并没有把所有的属性都列出来,但是它提供了一个嵌套的Configuration类型字段,通过这种方式可以直接对Configuration对象进行属性配置
    mybatis.configuration.lazy-loading-enabled=true
    mybatis.configuration.aggressive-lazy-loading=true
    
  • 基本上大部分配置都可以通过这种形式去实现,如果遇到不会配置的内容,仍然可以通过mybatis-config.xml方式去配置MyBatis,然后在Spring Boot的配置中指定该文件的路径即可
    ## application.properties
    mybatis.config-location=classpath:mybatis-config.xml
    
  • 在注入Mapper接口时,建议使用@Autowired注解,根据类型注入一定不会发生bean冲突
  • 在任何框架中使用MyBatis都只是配置不同而已,其核心用法是一样的,因此学会MyBatis的用法才是最主要的

第11章 MyBatis开源项目

参考官方提供的示例是了解各自集成框架和MyBatis用法的最好方式。以GitHub上的开源项目为基础可以使我们获得更高的起点

Git与GitHub入门

  • 在使用Maven进行依赖管理的项目中,有用的文件基本上就是src目录和pom.xml文件。项目根目录下由IDE生成的大量和IDE配置相关的文件,对于项目来说都是无关紧要的东西,只对当前电脑和IDE有用。在Git中配置忽略文件很简单,在项目顶层目录或具体某个目录中添加名为".ignore"的文件即可
  • .ignore文件中一个* 符号用于匹配文件名称的一部分或完整的文件名,两个* 符号用于匹配0个或多个目录
  • 在git中,服务器端的公共仓库必须是一个裸(不是工作目录)仓库
    $ cd xxxx
    $ git init --bare
    Initialized empty Git repository in xxxxx
    
  • 每次和服务器交互时,如果repo地址的名字很长就会不方便,可以通过如下代码给当前工作空间添加一个远程的源(相当于别名):$ git remote add origin ***
  • git push -u origin master这个命令中,-u意思是--set-upstream,整条指令相当于git push origin master + git branch --set-upstream-to=origin/master master
  • 对于许多对测试有严格要求的项目,建议在开发过程中提供完善的测试,必要时提供相应的文档。良好的编码习惯会让项目开发者最大程度上合并我们的提交

MyBatis源码讲解

阅读MyBatis源码可以学到很多东西,如缓存的装饰模式、大量Builder类的建造者模式、拦截器的代理链调用等等

  • MyBatis官方项目基本都是使用Maven管理依赖的,所有的项目依赖都继承自parent项目,为了能够正常地编译MyBatis,我们把parent也clone到本地。该项目的官方介绍是

    MyBatis-Parent is the MyBatis parent POM which has to be inherited by all MyBatis modules

  • 默认情况下,MyBatis会按照默认的顺序去寻找日志实现类,只要找到了对应的依赖,就会停下来。日志的优先级顺序为SLF4J > Apache Commons Logging > Log4j2 > Log4j > JDK Logging > StdOut Logging > NO Logging
  • Configuration是MyBatis中最重要的一个类,几乎包含了MyBatis全部的内容,记录了MyBatis的各项属性配置。常见的settings中的配置基本都可以通过Configuration来完成
  • Executor对象是MyBatis底层执行数据库操作的直接对象,大多数MyBatis方便调用的方式都是对该对象的封装
  • 无论通过XML方式还是注解方式配置SQL语句,MyBatis中的SQL语句都会被封装成SqlSource对象。静态SQL对应StaticSqlSource,动态SQL被处理为DynamicSqlSource,使用Provider类注解标记的方法会生成ProviderSqlSource所有类型的SqlSource在最终执行前,都会被处理成StaticSqlSource
  • 在XML配置中经常使用的resultType映射方式,在MyBatis底层仍然是ResultMap对象
  • MappedStatement是对SQL更高层次的封装,这个对象包含了执行SQL所需的各种配置信息。使用MyBatis时,项目启动后就已经准备好了所有方法对应的MappedStatement对象,在执行MyBatis的数据库操作时,底层就是通过调用Executor相应的方法来执行的
  • 但是,通过Executor执行并不方便,因此可以再提高一个层次,进一步封装为SqlSession对象,使其更方便调用,如sqlSession.selectOne("tk.mybatis.sample.mapper.SampleMapper.selectCountry", 2L)
  • 更进一步,使用JDK动态代理的方式实现接口调用

MyBatis包简介

  • org.apache.ibatis.annotation,包含注解方式需要用到的所有注解
  • org.apache.ibatis.binding,绑定接口和映射语句,使用JDK动态代理实现
  • org.apache.ibatis.builder,映射语句的构造器,大量使用建造者模式
  • org.apache.ibatis.cache,缓存接口和缓存实现,还有许多缓存的装饰类,通过装饰模式提供复杂功能
  • org.apache.ibatis.cursor,游标接口和实现类,使用游标类型作为返回值可以按需取值
  • org.apache.ibatis.datasource,数据源相关,提供了UNPOOLED/POOLED/JNDI三种数据源
  • org.apache.ibatis.exceptions,异常类,不过除了这个包之外其他包中也有异常类
  • org.apache.ibatis.executor,包含了Executor接口和几个实现类,二级缓存就是通过CacheExecutor装饰类实现的。这个包还含有一些重要的子包
    • keygen,包含主键生成接口及其实现类
    • loader,延迟加载相关类,MyBatis通过对结果进行动态代理来实现关联和集合的延迟加载
    • parameter,参数赋值接口,MyBatis的参数最终会转换为底层的JDBC方式,这个接口用于对JDBC预编译语句进行赋值
    • result,ResultHandler接口的实现类,用于处理映射后的对象
    • resultset,ResultSetHanlder接口的实现类,用于处理ResultSet和结果映射类型的转换
    • statement,StatementHandler接口和相应的实现,是对JDBC中Statement接口的封装,支持普通方式调用、预编译方式调用、存储过程方式调用
  • org.apache.ibatis.io,最主要的两个类为ResourcesResolveUtil,用于获取资源,根据指定条件获取类
  • org.apache.ibatis.jdbc,JDBC工具类
  • org.apache.ibatis.lang
  • org.apache.ibatis.logging,日志接口和常用日志组件的实现,还包含了对JDBC底层ConnectionStatement等对象的日志代理
  • org.apache.ibatis.mapping,包含了ResultMapParameterMap等与映射相关的配置,还有对底层JDBC的封装
  • org.apache.ibatis.parsing,XML解析实现,可用于Mapper.xml和MyBatis配置文件的解析
  • org.apache.ibatis.plugin,包含了与插件相关的接口和注解
  • org.apache.ibatis.reflection,反射工具类,也是MyBatis结果映射最重要的工具类
  • org.apache.ibatis.scrpting,XML映射语句实现类,MyBatis通过自己的一套XML标记实现了动态SQL
  • org.apache.ibatis.session,SqlSession接口及实现类,还有主要功能类。这个包用于对ExecutorMappedStatement进行封装
  • org.apache.ibatis.transaction,事务接口和实现类,提供了JDBC事务和外部的事务管理
  • org.apache.ibatis.type,Java类型和JDBC类型转换器。如果要实现自定义类型转换器,可以参考此处介绍的大量示例

MyBatis测试用例

在学习MyBatis时,还有比源码更有价值的内容,那就是测试

  • MyBatis针对各个具体的类和功能提供了全面的测试以及详细的测试用例。在阅读源码的同时,还可以通过测试用例更直观地了解类的作用或用法
  • 为了方便测试,MyBatis基本上都选择内存数据库,如hsqldb

总结

读完全书,我简单总结如下:

  1. 全书大部分内容在讲MyBatis的用法,间或插播一点原理的简单介绍。通过这部分的实例介绍,结合官网文档,应该能熟练掌握MyBatis的常见用法
  2. 插件开发部分作者花了较大篇幅介绍(毕竟是分页插件PageHelper的作者,对这方面话题天然亲切),无奈目前功力尚浅,基本略过
  3. 对Git和GitHub的介绍篇幅也挺大,对不熟悉的人来说算是一个快速入门,但对于早已经熟悉的人来说有些冗余,可能这就是所谓众口难调吧,作者太难了。。。不过对于源码研究来说,获取源码并能够自行编译、调试是前提,作者这样安排也可以理解吧。
  4. 最后一章对于MyBatis源码包的介绍我觉得挺好,以鸟瞰视野观其大略,正好方便了我后续结合《MyBatis技术内幕》一起看

所以,最终我对这本书的定位是:适合用来入门,并通过日常案头查阅,不断熟悉MyBatis的各项用法