Mybatis:缓存,动态SQL,注解SQL以及动态标签使用

1 转义字符

字符 转义 描述
< &lt; 小于
<= &lt;= 小于等于
> &gt; 大于
>= &gt;= 大于等于
<> &lt;&gt; 不等于
& &amp;
' &apos;
" &quot;

2 一级缓存以及二级缓存

学习Mybatis缓存的过程中,发现一篇美团的优秀文章: 聊聊MyBatis缓存机制.
此处对一级缓存以及二级缓存的使用进行总结.

2.1 一级缓存

2.1.1 小结

(1) MyBatis一级缓存的生命周期和SqlSession一致;
(2) MyBatis一级缓存内部设计简单,只是一个没有容量限定HashMap,在缓存的功能性上有所欠缺;
(3) MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement.

2.1.2 一级缓存脏读现象

(1) 在使用MyBatis的项目中,被@Transactional注解的函数中,一个单独的SqlSession对象将会被创建和使用,所有数据库操作会共用这个sqlSession,当事务完成时,这个``sqlSession会以合适的方式提交或回滚; (2) SELECT语句默认开启查询缓存,并且不清除缓存,所以使用同一个 SqlSession 多次用相同的条件查询数据库时,只有第一次真实访问数据库,后面的查询都直接读取缓存返回; (3) 此时,如果其余SqlSession更新了带待查询数据,就会造成脏读现象; (4) 或者同一次SqlSession多次查询数据`,例如多次查询分表数据(查询结果和分表查询数据相关),就会造成查询结果失效.

2.2 二级缓存

(1) MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
(2) MyBatis多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
(3) 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用RedisMemcached等分布式缓存可能成本更低安全性也更高。

3 Myabtis枚举值转换与驼峰转换配置

3.1 枚举值转换配置

mybatis.configuration.default-enum-type-handler = org.apache.ibatis.type.EnumOrdinalTypeHandler

定义如上配置,则Mybatis存储枚举值时,添加/更新枚举值转换为Integer,查询时会自动将Integer转换为相应的枚举值.

3.2 驼峰转换配置

mybatis.configuration.map-underscore-to-camel-case = true

4.动态注解SQL

4.1 查询

4.1.1 单条查询

@Select("SELECT * FROM `attachment` WHERE `id` = #{id}")
Attachment getAttachment(@Param("id") Integer id);

4.1.2 列表查询

此处为了避免在注解的动态SQL中写foreach,使用辅助类完成字符串替换的工作.
查询出的结果Mybatis会自动将结果映射到返回值上,支持批量查询结果.

@Lang(SimpleSelectInExtendedLanguageDriver.class)
@Select("SELECT id, created_time FROM `change` WHERE id in (#{ids})")
List<Change> getChangeTimeInfo(@Param("ids") List<Integer> ids);

import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SimpleSelectInExtendedLanguageDriver  extends XMLLanguageDriver implements LanguageDriver {

    private final Pattern inPattern = Pattern.compile("\(#\{(\w+)\}\)");

    @Override
    public SqlSource createSqlSource(Configuration configuration,
                                     String script, Class<?> parameterType) {

        Matcher matcher = inPattern.matcher(script);
        if (matcher.find()) {
            script = matcher.replaceAll(
                    "(<foreach collection="$1" item="__item" separator="," >#{__item}</foreach>)");
        }
        script = "<script>" + script + "</script>";
        return super.createSqlSource(configuration, script, parameterType);
    }
}

4.2 插入数据

以下为示例使用的实体类:

import lombok.Data;

@Data
public class TaskTreeItem {
    private Integer id;
    /**
     * 当前节点的祖先
     */
    private Integer ancestor;
    //
    /**
     * 当前节点
     */
    private Integer descendant;
}

4.2.1 单条插入

单条插入数据,如果需要返回生成的主键值,可以设置useGeneratedKeys true,指定id为返回的主键值.

@Insert("INSERT INTO task_tree (ancestor,descendant,depth) VALUES (#{ancestor},#{descendant})")
@Options(useGeneratedKeys = true, keyColumn = "id")
int insert(TaskTreeItem taskTreeItem);

4.2.2 批量插入

批量插入需要写动态SQL(此外需要确保数据库支持),此处使用到<foreach>.

    @Insert({"<script>",
            "INSERT INTO `task_tree` (`ancestor`, `descendant`) VALUES ",
                "<foreach item='item' index='index' collection='list' open='' separator=',' close=''>",
                    "(#{item.ancestor}, #{item.descendant})",
                "</foreach>",
            "</script>"})
    Integer addTreeItems(List<TaskTreeItem> taskTreeItems);

4.3 更新数据

4.3.1 简单更新

    @Update("UPDATE `change` SET `status` = #{status}, `finish_time` = #{finishTime} WHERE `id` = #{id}")
    int finishChange(@Param("id") int id, @Param("status") Change.Status status, Instant finishTime);

在不涉及变量判断的情况下,通过@Param注解将参数映射到SQL语句中,即可实现数据的更新.

4.3.2 含逻辑判断更新

业务的数据表更新中,经常含有复杂的判断(诸如非空判断,时间戳比较等判断),因此在此处进行展示操作.
(业务场景中,在CRUD的各个环节均可能存在逻辑判断,此处只是节选作为说明).

    @Update("<script>" +
                "UPDATE `change` " +
                    "<set>" +
                        "<if test="description != null">" +
                            " description = #{description}," +
                        "</if> " +
                        "<if test="executePlan != null">" +
                            " execute_plan = #{executePlan}," +
                        "</if> " +
                        "<if test="status != null">" +
                            " status = #{status}," +
                        "</if> " +
                        " id = #{id}, " +
                    "</set>" +
                " WHERE id = #{id}" +
            "</script>")
    int updateChange(Change change);

在注解中使用动态SQL要比在XML中使用困难,主要在于维护字符串拼接以及字符串格式化.
动态SQL需要在开头以及结尾添加