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

2021-03-31 fishedee 后端

1 概述

MyBatis从入门到精通,简要地把MyBatis的知识都说了。MyBatis其实就是一个方便在xml文件写SQL的库而已,并没有很好地做好ORM的工作,只能算是一个半自动的ORM。优点是简单,可控,上手快。

2 SqlSession与SqlSessionFactory

2.1 裸写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>
        <setting name="logImpl" value="LOG4J"/>
        <setting name="cacheEnabled" value="false"/>
    </settings>
    <typeAliases>
            <package name="mybatis_test.model"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value=""/>
                    </transactionManager>
            <dataSource type="UNPOOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/Test"/>
                <property name="username" value="root"/>
                <property name="password" value="1"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="spring_test/mapper/CountryMapper.xml"/>
    </mappers>
</configuration>

首先写一个mybatis的配置文件,typeAliases是为了简化resultType中不需要写命名空间,因为它指定了默认查询那些包下面的resultType。

<?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="mybatis_test.mapper.CountryMapper">
    <select id="selectAll" resultType="Country">
        select id,countryName,countryCode from t_country
    </select>
    <insert id="add" useGeneratedKeys="true" keyProperty="id">
        insert into t_country(countryName,countryCode) values(#{countryName},#{countryCode})
    </insert>
    <delete id="del">
        delete from t_country where id = #{id}
    </delete>
    <update id="mod">
        update t_country set countryName=#{countryName},countryCode=#{countryCode} where id = #{id}
    </update>
</mapper>

然后我们写了CountryMapper.xml,相当于用xml的方式写SQL代码。

private SqlSessionFactory sqlSessionFactory;

private  void init(){
    try {
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        reader.close();
    }catch(Exception e){
        e.printStackTrace();
    }
}

然后我们创建一个SqlSessionFactory

private void printCountryList(List<Country> countryList){
    for( Country country : countryList){
        System.out.printf("id:%s,name:%s,code:%s\n",country.id,country.countryName,country.countryCode);
    }
}

private void showCountryList(SqlSession sqlSession){
    List<Country> countryList = sqlSession.selectList("spring_test.mapper.CountryMapper.selectAll");

    printCountryList(countryList);
}

private void addCountry(SqlSession sqlSession){
    Country country = new Country();
    country.countryCode = "67";
    country.countryName = "我的国";

    sqlSession.insert("spring_test.mapper.CountryMapper.add",country);
}

private void modCountry(SqlSession sqlSession,Long id){
    Country country = new Country();
    country.id = id;
    country.countryCode = "88";
    country.countryName = "他的国";

    sqlSession.update("spring_test.mapper.CountryMapper.mod",country);
}

private void delCountry(SqlSession sqlSession,Long id){
    sqlSession.delete("spring_test.mapper.CountryMapper.del",id);
}
private void test1(){

    System.out.println("---- test1 ----");

    SqlSession sqlSession = this.sqlSessionFactory.openSession();

    try {
        showCountryList(sqlSession);

        addCountry(sqlSession);

        delCountry(sqlSession, 3L);

        modCountry(sqlSession, 2L);

        showCountryList(sqlSession);
    }finally {
        sqlSession.rollback();
    }
}

执行语句的时候,我们总是首先用sqlSessionFactory的openSession来获取sqlSession。最后执行sqlSession的select,update,delete,insert来执行具体的方法。这些方法的第一个参数都是xml中命名空间+id的位置,然后传入参数即可。

private void showCountryListNow(){
    SqlSession sqlSession2 = this.sqlSessionFactory.openSession();
    showCountryList(sqlSession2);
    sqlSession2.close();
}
private void test2(){

    System.out.println("---- test2 ----");

    SqlSession sqlSession = this.sqlSessionFactory.openSession();


    try {
        showCountryList(sqlSession);

        addCountry(sqlSession);

        //在sqlSession没有提交的时候,这个时候用另外一个sqlSession读取出来的数据依然是没有添加进去的
        System.out.println("提交前");
        showCountryListNow();

        sqlSession.commit();
        sqlSession.close();

        //在sqlSession提交以后,这个时候用另外一个sqlSession读取出来的数据才是有数据的
        //注意,两次读取必须用不同的sqlSession,否则会因为一级缓存读取出来的数据都是相同,无刷新的
        System.out.println("提交后");
        showCountryListNow();
    }finally {
    }
}
---- test2 ----
DEBUG [main] -==>  Preparing: select id,countryName,countryCode from t_country 
DEBUG [main] -==> Parameters: 
TRACE [main] -<==    Columns: id, countryName, countryCode
TRACE [main] -<==        Row: 1, 中国, CN
TRACE [main] -<==        Row: 2, 美国, US
TRACE [main] -<==        Row: 3, 俄罗斯, RU
TRACE [main] -<==        Row: 4, 英国, GB
TRACE [main] -<==        Row: 5, 法国, FR
TRACE [main] -<==        Row: 6, MK世界, MK
TRACE [main] -<==        Row: 7, MK世界, MK
TRACE [main] -<==        Row: 8, MK世界, MK
DEBUG [main] -<==      Total: 8
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
DEBUG [main] -==>  Preparing: insert into t_country(countryName,countryCode) values(?,?) 
DEBUG [main] -==> Parameters: 我的国(String), 67(String)
DEBUG [main] -<==    Updates: 1
提交前
DEBUG [main] -==>  Preparing: select id,countryName,countryCode from t_country 
DEBUG [main] -==> Parameters: 
TRACE [main] -<==    Columns: id, countryName, countryCode
TRACE [main] -<==        Row: 1, 中国, CN
TRACE [main] -<==        Row: 2, 美国, US
TRACE [main] -<==        Row: 3, 俄罗斯, RU
TRACE [main] -<==        Row: 4, 英国, GB
TRACE [main] -<==        Row: 5, 法国, FR
TRACE [main] -<==        Row: 6, MK世界, MK
TRACE [main] -<==        Row: 7, MK世界, MK
TRACE [main] -<==        Row: 8, MK世界, MK
DEBUG [main] -<==      Total: 8
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
提交后
DEBUG [main] -==>  Preparing: select id,countryName,countryCode from t_country 
DEBUG [main] -==> Parameters: 
TRACE [main] -<==    Columns: id, countryName, countryCode
TRACE [main] -<==        Row: 1, 中国, CN
TRACE [main] -<==        Row: 2, 美国, US
TRACE [main] -<==        Row: 3, 俄罗斯, RU
TRACE [main] -<==        Row: 4, 英国, GB
TRACE [main] -<==        Row: 5, 法国, FR
TRACE [main] -<==        Row: 6, MK世界, MK
TRACE [main] -<==        Row: 7, MK世界, MK
TRACE [main] -<==        Row: 8, MK世界, MK
TRACE [main] -<==        Row: 10, 我的国, 67
DEBUG [main] -<==      Total: 9
id:1,name:中国,code:CN
id:2,name:美国,code:US
id:3,name:俄罗斯,code:RU
id:4,name:英国,code:GB
id:5,name:法国,code:FR
id:6,name:MK世界,code:MK
id:7,name:MK世界,code:MK
id:8,name:MK世界,code:MK
id:10,name:我的国,code:67

那么,sqlSession究竟是什么,我们在test2中可以看出,sqlSession可以看做是事务的边界,只有sqlSession的commit以后,其他的sql连接才能看到这个sqlSession所做成的修改。

2.2 MyBatis的接口映射

代码在这里

package mybatis_test.mapper;

import mybatis_test.model.Country;

import java.util.List;

/**
 * Created by fish on 2021/3/24.
 */
public interface CountryMapper {
    List<Country> selectAll();
    void add(Country country);
    void mod(Country country);
    void del(Long id);
}

我们创建了一个CountryMapper的接口。其他的和2.1节的一样,建立SqlSessionFactory

private void printCountryList(List<Country> countryList){
    for( Country country : countryList){
        System.out.printf("id:%s,name:%s,code:%s\n",country.id,country.countryName,country.countryCode);
    }
}

private void showCountryList(CountryMapper countryMapper){
    List<Country> countryList = countryMapper.selectAll();

    printCountryList(countryList);
}

private void addCountry(CountryMapper countryMapper){
    Country country = new Country();
    country.countryCode = "67";
    country.countryName = "我的国";

    countryMapper.add(country);
}

private void modCountry(CountryMapper countryMapper,Long id){
    Country country = new Country();
    country.id = id;
    country.countryCode = "88";
    country.countryName = "他的国";

    countryMapper.mod(country);
}

private void delCountry(CountryMapper countryMapper,Long id){
    countryMapper.del(id);
}


private void test1(){

    System.out.println("---- test1 ----");

    SqlSession sqlSession = this.sqlSessionFactory.openSession();

    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);

    try {
        showCountryList(countryMapper);

        addCountry(countryMapper);

        delCountry(countryMapper, 3L);

        modCountry(countryMapper, 2L);

        showCountryList(countryMapper);
    }finally {
        sqlSession.rollback();
    }
}

与2.1的test1函数一样,我们这个时候用sqlSessionFactory来获取sqlSession,然后用sqlSession来根据CountryMapper接口来生成对应的实现。根据接口生成实现,是通过对接口所在的包名和类名,找到对应的命名空间的xml文件,并且将xml文件的id与方法名一一对应来生成的。值得注意的是,CountryMapper依然与单个的指定的sqlSession绑定。

private void showCountryListNow(){
    SqlSession sqlSession2 = this.sqlSessionFactory.openSession();
    CountryMapper countryMapper = sqlSession2.getMapper(CountryMapper.class);
    showCountryList(countryMapper);
    sqlSession2.close();
}

private void test2(){

    System.out.println("---- test2 ----");

    SqlSession sqlSession = this.sqlSessionFactory.openSession();

    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);

    try {
        showCountryList(countryMapper);

        addCountry(countryMapper);

        //CountryMapp是与sqlSession绑定的,sqlSession未提交就,CountryMapper所做的修改也不会提交
        System.out.println("提交前");
        showCountryListNow();

        sqlSession.commit();
        sqlSession.close();

        //sqlSession提交以后,CountryMapper的修改才会提交
        System.out.println("提交后");
        showCountryListNow();
    }finally {
    }
}

因为CountryMapper依然与单个的指定的sqlSession绑定,所以,只有sqlSession执行commit以后,这个接口所做的sql修改才会提交。而且,当这个sqlSession关闭以后,再次调用CountryMapper是会报错的,因为该CountryMapper所依赖的sqlSession早就关闭了。

2.3 集成Spring Boot

代码在这里

/**
 * Created by fish on 2021/3/15.
 */
spring.datasource.driver-class-name =  com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/Test
spring.datasource.username = root
spring.datasource.password = 1

mybatis.mapper-locations = classpath:mapper/*.xml
mybatis.type-aliases-package=mybatis_test.model
mybatis.config-location=classpath:mybatis-config.xml

logging.level.mybatis_test.mapper=DEBUG

我们直接引用mybatis的starter就可以了,同时配置一下外部的mybatis的外部配置文件。值得注意的是,spring boot默认的日志系统时slf4j的接口,logback的实现。

<?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>
        <setting name="logImpl" value="SLF4J"/>
        <setting name="cacheEnabled" value="false"/>
    </settings>
</configuration>

因此,我们要指定mybatis的日志实现为SLF4J,才会让mybatis的日志输出也用到了spring boot的日志系统。

<?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="mybatis_test.mapper.CountryMapper">
    <select id="selectAll" resultType="Country">
        select id,countryName,countryCode from t_country
    </select>
    <insert id="add" useGeneratedKeys="true" keyProperty="id">
        insert into t_country(countryName,countryCode) values(#{countryName},#{countryCode})
    </insert>
    <delete id="del">
        delete from t_country where id = #{id}
    </delete>
    <update id="mod">
        update t_country set countryName=#{countryName},countryCode=#{countryCode} where id = #{id}
    </update>
</mapper>

mapper文件依然不变,因为配置文件中指定了mybatis.mapper-locations = classpath:mapper/*.xml,所以它会在启动的时候,自动扫描注册resources文件夹下面的mapper的所有xml文件。

package mybatis_test.mapper;

import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * Created by fish on 2021/3/24.
 */
@Mapper
public interface CountryMapper {
    List<Country> selectAll();
    void add(Country country);
    void mod(Country country);
    void del(Long id);
}

然后,我们再定义一个接口,注意,必须要用@Mapper注解。

@Autowired
private CountryMapper countryMapper;

private void printCountryList(List<Country> countryList){
    for( Country country : countryList){
        logger.info("id:{},name:{},code:{}",country.id,country.countryName,country.countryCode);
    }
}

private void showCountryList(){
    List<Country> countryList = countryMapper.selectAll();

    printCountryList(countryList);
}

private void addCountry(){
    Country country = new Country();
    country.countryCode = "67";
    country.countryName = "我的国";

    countryMapper.add(country);
}

private void modCountry(Long id){
    Country country = new Country();
    country.id = id;
    country.countryCode = "88";
    country.countryName = "他的国";

    countryMapper.mod(country);
}

private void delCountry(Long id){
    countryMapper.del(id);
}


@Transactional
private void test1(){

    System.out.println("---- test1 ----");

    showCountryList();

    addCountry();

    delCountry(3L);

    modCountry(2L);

    showCountryList();

    throw new RuntimeException("throw by me");
}

这个时候,直接注入CountryMapper的接口就能用了,不需要创建sqlSessionFactory,也不需要创建sqlSession,更不需要用sqlSession来创建对应的CountryMapper接口的实现。

@Autowired
SqlSessionFactory sqlSessionFactory;

private void showCountryListNow(){
    SqlSession sqlSession2 = this.sqlSessionFactory.openSession();
    CountryMapper countryMapper = sqlSession2.getMapper(CountryMapper.class);
    List<Country> countryList = countryMapper.selectAll();
    printCountryList(countryList);
    sqlSession2.close();
}

private void test2(){

    System.out.println("---- test2 ----");

    showCountryList();

    //这里的CountryMapper绑定的不是普通的sqlSession,它绑定的是sqlSessionTemplate类
    //SqlSessionTemplate的特点是与事务绑定,当没有事务的时候,CountryMapper一个方法执行完毕后会自动commit
    //当有事务的时候,就会在事务提交的时候进行commit
    //具体看SqlSessionTemplate sqlSessionTemplate;的实现
    //参考资料:
    // * https://blog.csdn.net/yu_kang/article/details/88941908
    // * https://blog.csdn.net/xlgen157387/article/details/79438676
    addCountry();

    //所以,这里不需要sqlSession.commit操作,在其他线程中就能看到执行了country添加的结果
    showCountryListNow();
}

而且,更神奇的是,CountryMapper在2.2节我们探讨过,它是与具体的sqlSession绑定在一起的,只有sqlSession进行commit了,CountryMapper所做的修改才会提交。但是,在Spring Boot的集成,直接执行CountryMapper的方法就会自动commit,我们找不到CountryMapper对应的sqlSession,更不用说要用它来commit了。这是因为,这个CountryMapper绑定的不是DefaultSqlSession,而是SqlSessionTemplate。

SqlSessionTemplate的特点是:

  • 无事务状态下(无@Transactional注解),每个方法会自动创建一个SqlSession,并且方法执行完毕后自动commit。因此,你依旧能在一个id下执行多个sql操作。
  • 有事务状态(有@Transactional注解),每个方法会沿用一个固定的SqlSession,并且在事务提交的时候,commit对应的单个SqlSession。

这里的代码比较神奇,SqlSessionTemplate的主要实现是依赖TransactionSynchronizationManager的功能,在事务状态下:

  • 将首次使用的SqlSession用TransactionSynchronizationManager.setResource方法写进去,下次当前事务的其他方法用sqlSession的时候,用TransactionSynchronizationManager.getResource获取出来的,这样达到了同一个事务下,总是固定用同一个sqlSession的目的。
  • 使用TransactionSynchronizationManager.registerSynchronization方法,当事务提交之前的一刻(beforeCommit)回调自己的通知器,将TransactionSynchronizationManager.getResource的sqlSession拿出来,commit即可。

可以看到,TransactionSynchronizationManager可以轻松实现多个数据源的同步提交功能。

最后,谈谈SqlSessionManager与SqlSessionTemplate的不同。SqlSessionManager是线程级的,SqlSessionTemplate是事务级的。SqlSessionManager在同一个线程下总是使用同一个sqlSession。但是,在Web开发中,数据库写入读取操作之间总会穿插着外部第三方接口,耗时计算等业务代码,如果用SqlSessionManager就会导致挂起数据库连接,等待第三方接口返回,等待耗时计算执行完毕,才能释放数据库连接的问题。

3 SQL参数传入

代码在这里

3.1 函数参数名称

@Mapper
public interface CountryMapper2 {
    List<Country> selectByCodeAndName(String countryName,String countryCode);
}
<select id="selectByCodeAndName" resultType="Country">
    select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}
</select>

注意,这种想当然地直接使用函数入参的名称是不行的。Java编译代码以后,参数名称都变为arg1,arg2的形式,原来的参数名称已经丢弃了。

3.2 @Param注解

@Mapper
public interface CountryMapper2 {
    List<Country> selectByCodeAndNameWithParam(@Param("countryName") String countryName, @Param("countryCode") String countryCode);
}
<select id="selectByCodeAndNameWithParam" resultType="Country">
    select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}
</select>

用@Param注解来传入参数才是正确的。

3.3 参数为类类型

List<Country> selectByCodeAndNameWithClass(CountryCodeAndName param);
<select id="selectByCodeAndNameWithClass" resultType="Country">
    select id,countryName,countryCode,createTime,modifyTime from t_country where countryName = #{countryName} and countryCode = #{countryCode}
</select>
int add(Country country);
<insert id="add" useGeneratedKeys="true" keyProperty="id">
    insert into t_country(countryCode,countryName,createTime,modifyTime)VALUES
    (#{countryCode},
    #{countryName},
    #{createTime,jdbcType=TIMESTAMP},
    #{modifyTime,jdbcType=TIMESTAMP});
</insert>

在添加操作的时候,我们依然可以直接类类型的属性。

3.4 参数为Map类型

当传入的是一个类的时候,我们可以轻松地直接引用类的属性。

int updateByMap(Map<String,Object> map);
<update id="updateByMap">
    update t_country set
    <foreach collection="_parameter" item="val" index="key" separator=",">
        ${key} = #{val}
    </foreach>
    where id = #{id}
</update>

当传入的是map类型的时候,我们可以用固定的参数名称_parameter来获取唯一的入参,这样能引用到map类型了。

int updateByMapAndId(@Param("map") Map<String,Object> map,@Param("id") Long id);
<update id="updateByMapAndId">
    update t_country set
    <foreach collection="map" item="val" index="key" separator=",">
        ${key} = #{val}
    </foreach>
    where id = #{id}
</update>

当入参不止一个,有map类型和基础类型的时候,就不能再通过_parameter来获取了,我们只能用@Param注解来表明参数的名称。

3.5 参数为List类型

List<Country> selectByCountryCodeList(List<String> countryCodeList);
<select id="selectByCountryCodeList" resultType="Country">
    select id,countryName,countryCode,createTime,modifyTime from t_country
    where countryCode in
    <foreach collection="list" open="(" close=")" separator=","
        item="countryCode" index="i">
        #{countryCode}
    </foreach>
</select>

我们可以用固定的参数名称list,来指定这个List类型的参数

4 动态SQL语句

代码在这里

4.1 if标签

<insert id="insertCheckName" useGeneratedKeys="true" keyProperty="id">
    insert into t_country(countryName,countryCode)values(
    <if test="countryName != null and countryName.length() != 0">
        #{countryName},
    </if>
    <if test="countryName == null or countryName.length() == 0">
        "UNKNOWN",
    </if>
    #{countryCode}
    )
</insert>

if里面的test属性是特殊的OGNL语法,不好用的地方是没有else标签。

4.2 where标签

<select id="selectByWhere" resultType="Country">
    select id,countryName,countryCode,createTime,modifyTime from t_country
    <where>
        <if test="id != 0">
            and id = #{id}
        </if>
        <if test="countryName != null and countryName.length() != 0">
            and countryName = #{countryName}
        </if>
        <if test="countryCode != null and countryCode.length() != 0">
            and countryCode = #{countryCode}
        </if>
    </where>
</select>

我们可以在where里面附加多个if标签,就可以做任意参数组合的模糊匹配查询。这个where标签的好处是,每个if里面都可以写一个and语句,即使只有一个匹配时生成的SQL也不会出错。

4.3 set标签

<update id="updateByIdSelective">
    update t_country
    <set>
        <if test="countryName != null and countryName.length() != 0">
            countryName = #{countryName},
        </if>
        <if test="countryCode != null and countryCode.length() != 0">
            countryCode = #{countryCode},
        </if>
        id = #{id},
    </set>
    where id = #{id}
</update>

同样地,set标签,相当于代替了select中的where标签。这样就能实现,任意非空属性的单独设值。但是,这个set标签与where标签不同的是,如果没有一个属性匹配的时候,生成的update语句就会缺少set部分内容而失败。因此,我们总是固定在set标签的尾部,加入id=#{id}的内容,而避免这个问题。

4.4 foreach标签

int insertList(List<Country> countryList);
<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
    insert into t_country(countryName,countryCode) VALUES
    <foreach collection="list" item="country" separator=",">
        (#{country.countryName},#{country.countryCode})
    </foreach>
</insert>

批量插入的方法,可以遍历一个List来批量插入数据。注意我们使用了useGeneratedKeys=true的设置,因此插入的自增字段会自动赋值到输入的参数countryList的id属性下面。keyProperty指定的是要返回的自增ID要插入到实例的哪个字段上。

4.5 bind标签

package mybatis_test;

import mybatis_test.model.Country;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by fish on 2021/3/28.
 */
public class StringUtil {
    final static Logger logger= LoggerFactory.getLogger(StringUtil.class);
    public static void print(Country country){
        logger.info("MyBatis print countryCode:{},countryName:{}",country.countryCode,country.countryName);
    }
}
<insert id="insertListWithPrint" useGeneratedKeys="true" keyProperty="id">
    insert into t_country(countryName,countryCode) VALUES
    <foreach collection="list" item="country" separator=",">
        (#{country.countryName},#{country.countryCode})
        <bind name="print" value="@mybatis_test.StringUtil@print(country)"/>
    </foreach>
</insert>

bind语句可以使用ONGL来执行Java的方法,调试的时候特别好用。

5 SQL返回值映射

代码在这里

5.1 ResultType的直接映射

package mybatis_test.model;

import java.util.Date;

/**
 * Created by fish on 2021/3/29.
 */
public class People {
    public Long peopleId;

    public String name;

    public String homeAddress;

    public String primaryEmail;

    public Long countryId;

    public Date createTime;

    public Date modifyTime;

    public String toString(){
        return String.format("People{id:%s,name:%s,homeAddress:%s,primaryEmail:%s,countryId:%s}",peopleId,name,homeAddress,primaryEmail,countryId);

    }
}

这是People类型,可以用setter,也可以直接用public字段。

package mybatis_test.mapper;

import mybatis_test.model.People;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * Created by fish on 2021/3/29.
 */
@Mapper
public interface PeopleMapper {
    List<People> selectAll();
}

这是PeopleMapper的接口

<?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="mybatis_test.mapper.PeopleMapper">
    <select id="selectAll" resultType="people">
        select people_id,name,home_address,primary_email,countryId,createTime,modifyTime from t_people;
    </select>
    <select id="selectByCountryId" resultType="people">
        select people_id,name,home_address,primary_email,countryId,createTime,modifyTime from t_people where countryId = #{countryId};
    </select>
</mapper>

这是PeopleMapper的xml实现,返回结果直接用resultType映射,相当于字段的一一映射。注意,sql字段都是下划线命名,但是属性字段是驼峰命名的,但是依然能匹配上。

<?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>
        <setting name="logImpl" value="SLF4J"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="cacheEnabled" value="false"/>
    </settings>
</configuration>

这是因为我们在mybatis-config.xml中配置了打开mapUnderscoreToCamelCase,这大大简化了开发的代码。

5.2 ResultMap的setter映射

package mybatis_test.model;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**
 * Created by fish on 2021/3/23.
 */
/* resultMap也支持属性的直接赋值,绕过setter赋值
public class Country{
    public Long countryId;

    public String name;

    public String code;

    public Long continentId;

    public Date createTime;

    public Date modifyTime;

    public String toString(){
        return String.format("Country{id:%s,name:%s,code:%s,continentId:%s}",countryId,name,code,continentId);

    }
}
*/
public class Country {
    final Logger logger = LoggerFactory.getLogger(getClass());
    private Long countryId;

    public void setCountryId(Long countryId){
        this.countryId = countryId;
    }

    public Long getCountryId(){
        return this.countryId;
    }

    private String name;

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return this.name;
    }

    private String code;

    public void setCode(String code){
        this.code = code;
    }

    public String getCode(){
        return this.code;
    }

    private Long continentId;

    public void setContinentId(Long continentId){
        logger.info("setContinentId setter call {}",continentId);
        this.continentId = continentId;
    }

    public Long getContinentId(){
        return this.continentId;
    }

    private Date createTime;

    public void setCreateTime(Date createTime){
        this.createTime = createTime;
    }

    public Date getCreateTime(){
        return this.createTime;
    }

    private Date modifyTime;

    public void setModifyTime(Date modifyTime){
        this.modifyTime = modifyTime;
    }

    public Date getModifyTime(){
        return this.modifyTime;
    }

    public String toString(){
        return String.format("Country{id:%s,name:%s,code:%s,continentId:%s}",countryId,name,code,continentId);

    }
}

一个Country的定义,注意有了setter和getter方法。

package mybatis_test.mapper;

import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

/**
 * Created by fish on 2021/3/27.
 */
@Mapper
public interface CountryMapper {
    List<Country> selectAll();
}

定义CountryMapper的接口

<?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="mybatis_test.mapper.CountryMapper">
    <resultMap id="countryMap" type="Country">
        <id property="countryId" column="countryId"/>
        <result property="name" column="name"/>
        <result property="code" column="code"/>
        <result property="continentId" column="continentId"/>
        <result property="createTime" column="createTime"/>
        <result property="modifyTime" column="modifyTime"/>
    </resultMap>
    <select id="selectAll" resultMap="countryMap">
        select countryId,name,code,continentId,createTime,modifyTime from t_country;
    </select>
</mapper>

使用resultMap来指定如何将列映射到字段上面即可

5.3 ResultMap的contructor映射

package mybatis_test.model;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**
 * Created by fish on 2021/3/29.
 */
public class Continent {
    final Logger logger = LoggerFactory.getLogger(getClass());

    private Long continentId;

    private String name;

    private Date createTime;

    private Date modifyTime;

    public Continent(Long continentId,String name,Date createTime,Date modifyTime){
        logger.info("continent construct call");
        this.continentId = continentId;
        this.name = name;
        this.createTime = createTime;
        this.modifyTime = modifyTime;
    }

    public Long getContinentId(){
        return this.continentId;
    }

    public String getName(){
        return this.name;
    }

    public Date getCreateTime(){
        return this.createTime;
    }

    public Date getModifyTime(){
        return this.modifyTime;
    }

    public String toString(){
        return String.format("Continent{id:%s,name:%s}",continentId,name);
    }
}

定义一个Continent,注意只有getter,没有setter,所有字段通过Continent的构造函数传入,这意味着这个类无法被修改。

package mybatis_test.mapper;

import mybatis_test.model.Continent;
import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * Created by fish on 2021/3/29.
 */
@Mapper
public interface ContinentMapper {
    List<Continent> selectAll();
}

这是ContinentMapper的接口

<?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="mybatis_test.mapper.ContinentMapper">
    <resultMap id="continentMap" type="Continent">
        <constructor>
            <idArg  javaType="Long" column="continentId"/>
            <arg javaType="String" column="name"/>
            <arg javaType="Date" column="createTime"/>
            <arg javaType="Date" column="modifyTime"/>
        </constructor>
    </resultMap>
    <select id="selectAll" resultMap="continentMap">
        select continentId,name,createTime,modifyTime from t_continent
    </select>
    <select id="selectByContinentId" resultMap="continentMap">
         select continentId,name,createTime,modifyTime from t_continent where continentId = #{id}
    </select>
</mapper>

指定resultMap来赋值数值,但是用constructor的方式来传入。注意,这个方法,缺少对函数参数的指定,只能指定函数的类型,要保证参数的顺序,要与构造器的参数顺序一致。

5.4 一对一映射

package mybatis_test.model;

/**
 * Created by fish on 2021/3/29.
 */
public class CountryAndContinent extends Country {

    private Continent continent;

    public void setContinent(Continent continent){
        this.continent = continent;
    }

    public String toString(){
        return String.format("CountryAndContinent{%s,continent:%s}",super.toString(),this.continent);
    }
}

先定义一个CountryAndContinent类,它包含着一个关联类Continent。

<resultMap id="countryAndContinentMap" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndContinent">
    <association property="continent" columnPrefix="conti_" resultMap="mybatis_test.mapper.ContinentMapper.continentMap"/>
</resultMap>
<select id="selectAll" resultMap="countryAndContinentMap">
    select
    t_country.countryId ,
    t_country.name ,
    t_country.code ,
    t_country.createTime ,
    t_country.modifyTime ,
    t_continent.continentId  as conti_continentId,
    t_continent.name as conti_name,
    t_continent.createTime as conti_createTime,
    t_continent.modifyTime as conti_modifyTime
    from t_country
    inner join t_continent on t_country.continentId = t_continent.continentId;
</select>

第一种方法是,拉取的时候,直接join,将多个类的数据取出来。然后在resultMap里面,指定association,将conti_开头的列都赋值到continent属性上。

<resultMap id="countryAndContinentMap2" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndContinent">
    <association property="continent" column="{id=continentId}" select="mybatis_test.mapper.ContinentMapper.selectByContinentId"/>
</resultMap>
<select id="selectAllWithNest" resultMap="countryAndContinentMap2">
    select countryId,name,code,createTime,modifyTime,continentId from t_country;
</select>

第二种方法是,分两次select拉取,先用select拉取一次,然后对于每一个continentId,用selectByContinentId再拉取多一次,赋值到continent属性上。显然,这个方法有N+1的问题,每一个country都需要执行一次selectByContinentId操作

5.5 一对多映射

package mybatis_test.model;

import java.util.List;

/**
 * Created by fish on 2021/3/30.
 */
public class CountryAndPeople extends Country{
    private List<People> peopleList;

    public void setPeopleList(List<People> peopleList){
        this.peopleList = peopleList;
    }

    public String toString(){
        return String.format("CountryAndPeople{%s,people:%s}",super.toString(),this.peopleList);
    }
}

先定义一个CountryAndPeople,嵌套了一个peopleList的List。这是一个一对多的映射

<resultMap id="peopleMap" type="People">
    <id property="peopleId" column="people_id"/>
    <result property="name" column="name"/>
    <result property="homeAddress" column="home_address"/>
    <result property="primaryEmail" column="primary_email"/>
    <result property="countryId" column="countryId"/>
    <result property="createTime" column="createTime"/>
    <result property="modifyTime" column="modifyTime"/>
</resultMap>
<resultMap id="countryAndPeopleMap" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndPeople">
    <association property="peopleList" columnPrefix="people_" resultMap="peopleMap"/>
</resultMap>
<select id="selectAll" resultMap="countryAndPeopleMap">
    select
    t_country.countryId ,
    t_country.name ,
    t_country.code ,
    t_country.createTime ,
    t_country.modifyTime ,
    t_people.people_id  as people_people_id,
    t_people.name as people_name,
    t_people.home_address as people_home_address,
    t_people.primary_email as people_primary_email,
    t_people.countryId as people_countryId,
    t_people.createTime as people_createTime,
    t_people.modifyTime as people_modifyTime
    from t_country
    left join t_people on t_people.countryId = t_country.countryId;
</select>

第一种方法依然是使用join来一次性全部拉取,然后使用columnPrefix属性,将部分列写入到peopleList属性上。

<resultMap id="countryAndContinentMap2" extends="mybatis_test.mapper.CountryMapper.countryMap" type="CountryAndPeople">
    <association property="peopleList" column="{countryId=countryId}" select="mybatis_test.mapper.PeopleMapper.selectByCountryId"/>
</resultMap>
<select id="selectAllWithNest" resultMap="countryAndContinentMap2">
    select countryId,name,code,createTime,modifyTime,continentId from t_country;
</select>

第二种方法是使用两次select来拉数据,第一次先拉t_country,第二次用selectByCountryId来拉每个country里面的people。显然,这个方法依然有N+1的问题。

6 缓存

代码在这里

6.1 一级缓存

package mybatis_test.model;

import java.io.Serializable;
import java.util.Date;

/**
 * Created by fish on 2021/3/23.
 */
public class Country implements Serializable {
    public Long id;

    public String countryName;

    public String countryCode;

    public Date createTime;

    public Date modifyTime;
}

先定义一个Country类

package mybatis_test.mapper;

import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

/**
 * Created by fish on 2021/3/27.
 */
@Mapper
public interface CountryMapper {
    List<Country> selectAll();
    Country selectById(@Param("id")Long countryId);
    Country selectById2(@Param("id")Long countryId);
    int insertList(List<Country> countryList);
}

再定义一个CountryMapper

<?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="mybatis_test.mapper.CountryMapper">
    <select id="selectAll" resultType="Country">
        select id,countryName,countryCode,createTime,modifyTime from t_country;
    </select>
    <select id="selectById" resultType="Country">
        select id,countryName,countryCode,createTime,modifyTime from t_country where id = #{id};
    </select>
    <select id="selectById2" resultType="Country">
        select id,countryName,countryCode,createTime,modifyTime from t_country where id = #{id};
    </select>
    <insert id="insertList" useGeneratedKeys="true" keyProperty="id">
        insert into t_country(countryName,countryCode) VALUES
        <foreach collection="list" item="country" separator=",">
            (#{country.countryName},#{country.countryCode})
        </foreach>
    </insert>
</mapper>

注意,selectById与selectById2的方法实现是完全一致的,只是方法名称不同而已。

@Transactional
public void test1(){
    logger.info("------ test1 ------");
    //在同一个事务里面,同一个方法用同样的参数返回的总是同一个对象
    Country country = countryMapper.selectById(1L);

    country.countryName = "CC";

    //这一步没有查询数据库,直接查一级缓存后返回了
    Country country2 = countryMapper.selectById(1L);

    logger.info("country pointer : {}, country2 pointer: {}",System.identityHashCode(country),System.identityHashCode(country2));
    logger.info("country name: {}, country2 name: {}",country.countryName,country2.countryName);

    //这一步有查询数据库,因为selectById和selectById2是不同的方法
    Country country2_2 = countryMapper.selectById2(1L);

    logger.info("country pointer : {}, country2_2 pointer: {}",System.identityHashCode(country),System.identityHashCode(country2_2));
    logger.info("country name: {}, country2_2 name: {}",country.countryName,country2_2.countryName);

    //但是,在同一个事务里面,用其他方法就不会返回这个对象.
    //所以,一级缓存总是以方法和参数作为缓存的key
    showCountryList();

    //添加一个Country,这时候会清空mapper所在的缓存
    Country countryNew = new Country();
    countryNew.countryName = "MK世界";
    countryNew.countryCode = "MK";
    List<Country> countriesAdd = new ArrayList<Country>();
    countriesAdd.add(countryNew);
    countryMapper.insertList(countriesAdd);

    //因为一级缓存为空,所以这次查询会走数据库.
    Country country3 = countryMapper.selectById(1L);
    logger.info("country pointer : {}, country3 pointer: {}",System.identityHashCode(country),System.identityHashCode(country3));
    logger.info("country name: {}, country3 name: {}",country.countryName,country3.countryName);
}
: ------ test1 ------
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?; 
==> Parameters: 1(Long)
<==      Total: 1
country pointer : 1789282489, country2 pointer: 1789282489
country name: CC, country2 name: CC
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?; 
==> Parameters: 1(Long)
<==      Total: 1
country pointer : 1789282489, country2_2 pointer: 1949126165
country name: CC, country2_2 name: 中国
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country; 
==> Parameters: 
<==      Total: 10
id:1,name:中国,code:CN,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:2,name:美国,code:US,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:3,name:俄罗斯,code:RU,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:4,name:英国,code:GB,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:5,name:法国,code:FR,createTime:Tue Mar 30 13:29:45 CST 2021,modifyTime:Tue Mar 30 13:29:45 CST 2021
id:6,name:MK世界,code:MK,createTime:Tue Mar 30 22:18:21 CST 2021,modifyTime:Tue Mar 30 22:18:21 CST 2021
id:7,name:MK世界,code:MK,createTime:Wed Mar 31 13:32:07 CST 2021,modifyTime:Wed Mar 31 13:32:07 CST 2021
id:8,name:MK世界,code:MK,createTime:Wed Mar 31 13:32:40 CST 2021,modifyTime:Wed Mar 31 13:32:40 CST 2021
id:10,name:我的国,code:67,createTime:Wed Mar 31 14:53:47 CST 2021,modifyTime:Wed Mar 31 14:53:47 CST 2021
id:11,name:MK世界,code:MK,createTime:Mon Apr 12 12:50:40 CST 2021,modifyTime:Mon Apr 12 12:50:40 CST 2021
==>  Preparing: insert into t_country(countryName,countryCode) VALUES (?,?) 
==> Parameters: MK世界(String), MK(String)
<==    Updates: 1
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country where id = ?; 
==> Parameters: 1(Long)
<==      Total: 1
country pointer : 1789282489, country3 pointer: 1733947537
country name: CC, country3 name: 中国

这是输出结果,注意,在同一个事务里面,以同一个方法和同一个参数返回的总是同一个对象,不管这个对象有没有提交,而且这种情况下会直接在缓存里面拿,不会去查询数据库。但是,不同方法的同一个参数,即使sql实现是相同的,也总是返回不同的对象。

另外,一级缓存是以单个Mapper为维度的,当这个Mapper里面执行了删除,修改,添加的操作时,总是会清空一级缓存。这个时候以同一个方法和同一个参数执行查询时,就会总是查询数据库,因为一级缓存是空的。

6.2 二级缓存

package mybatis_test.mapper;

import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * Created by fish on 2021/3/31.
 */
@Mapper
public interface CountryMapper2 {
    List<Country> selectAll();
}

定义一个CountryMapper2

<?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="mybatis_test.mapper.CountryMapper2">
    <cache
        eviction="FIFO"
        flushInterval="5000"
        size="512"
        readOnly="false"
    />
    <select id="selectAll" resultType="Country">
        select id,countryName,countryCode,createTime,modifyTime from t_country;
    </select>
</mapper>

这是带有二级缓存的设置,它表明缓存大小最大只有512个对象,而且每隔5000秒就会自动清空一次。当缓存满了以后,会以FIFO的方式来删除旧缓存。

public void test2(){
    logger.info("------ test2 ------");

    try{
        List<Country> countries = countryMapper2.selectAll();

        logger.info("countries select1 {}",countries.size());

        List<Country> countries2 = countryMapper2.selectAll();

        logger.info("countries select2 {}",countries2.size());

        Thread.sleep(5001,0);

        List<Country> countries3 = countryMapper2.selectAll();

        logger.info("countries select2 {}",countries3.size());
    }catch(Exception e){
        e.printStackTrace();
    }
}

这是执行代码,注意没有开启事务。

 ------ test2 ------
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.0
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country; 
==> Parameters: 
<==      Total: 11
countries select1 11
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.5
countries select2 11
Cache Hit Ratio [mybatis_test.mapper.CountryMapper2]: 0.3333333333333333
==>  Preparing: select id,countryName,countryCode,createTime,modifyTime from t_country; 
==> Parameters: 
<==      Total: 11
countries select2 11
Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@13acb0d1: startup date [Mon Apr 12 12:50:57 CST 2021]; root of context hierarchy
Unregistering JMX-exposed beans on shutdown
Unregistering JMX-exposed beans
HikariPool-1 - Shutdown initiated...
HikariPool-1 - Shutdown completed.

这是输出结果,第一次查询以后,后面的都会跳过数据库查询,直接用二级缓存的。当超时以后,二级缓存被清空了,就会去查询数据库。

7 分页插件

代码在这里

package mybatis_test.model;

import java.io.Serializable;
import java.util.Date;

/**
 * Created by fish on 2021/3/23.
 */
public class Country implements Serializable {
    public Long id;

    public String countryName;

    public String countryCode;

    public Date createTime;

    public Date modifyTime;

    public String toString(){
        return String.format("id:%s,name:%s,code:%s",id,countryName,countryCode);
    }
}

定义Country实体,它的特点是需要实现Serializable接口。

package mybatis_test.mapper;

import mybatis_test.model.Country;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

/**
 * Created by fish on 2021/3/27.
 */
@Mapper
public interface CountryMapper {
    List<Country> selectAll();
}

这是CountryMapper接口,注意接口上没有分页参数。

<?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="mybatis_test.mapper.CountryMapper">
    <select id="selectAll" resultType="Country">
        select id,countryName,countryCode,createTime,modifyTime from t_country;
    </select>
</mapper>

这是普通的select实现

public void test1(){
    logger.info("------ test1 ------");

    int pageIndex = 1;
    int pageSize = 10;
    String orderBy = "id asc";

    //分页信息
    PageHelper.startPage(pageIndex, pageSize, orderBy);

    PageInfo<Country> countryPageInfo = new PageInfo<Country>(countryMapper.selectAll());

    countryPageInfo.getList().stream().forEach(System.out::println);

    //打印分页信息
    System.out.println("当前页码:第" + countryPageInfo.getPageNum() + "页");
    System.out.println("分页大小:每页" + countryPageInfo.getPageSize() + "条");
    System.out.println("数据总数:共" + countryPageInfo.getTotal() + "条");
    System.out.println("总页数:共" + countryPageInfo.getPages() + "页");

}

这是执行代码,我们在selectAll之前调用一次PageHelper就能做分页了,相当的简单暴力。

8 总结

MyBatis的特点是:

  • 简单,清晰,其实就是一个简单的SQL模板引擎而已,加入了返回值映射,和set,where,if这些便利的标签。
  • 繁琐,抽象的程度很低,该写的SQL代码一行都没有少。当遇上简单的CRUD操作时,总是要在xml文件和mapper文件之间不断切换。

但是,MyBatis的设计最糟糕的问题在于缓存:

  • 一级缓存只能以单个方法和参数为缓存的key,它无法理解到不同方法返回的可能是同一个实体对象的问题。一级缓存的意义在于,写入操作时的批量性,可以只在事务commit的之前,才对数据库执行多个实体的批量update操作。但是,MyBatis的这个设计大大约束了一级缓存的批量update的能力。
  • 先走二级缓存,再走一级缓存。当二级缓存打开的时候,一级缓存失效,这个时候调用同一个方法和参数,会返回不同引用的对象。因为二级缓存返回的对象是反序列化生成出来的。这个基本上是个坑。
  • 二级缓存是在事务提交的时候保存到缓存的。所以,如果二级缓存读取出来的数据,在内存中修改了,但又没有对数据库进行update操作的话,会产生意外的错误缓存问题。详情看书本的P186页。

因此,二级缓存的使用场景必须局限在:

  • 无事务
  • 仅能依赖只读数据+interval缓存刷新来使用,不要使用缓存的通知刷新机制(当遇到UPDATE,INSERT和DELETE时就清空二级缓存,迷惑性很大,这样需要引入cache-ref的机制)
  • 有二级缓存的接口,切勿将数据读出来作为写操作的依据。

参考资料:

相关文章