• 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需要在开头以及结尾添加