SpringBoot+Mybatis 基于自定义TypeHandler 实现从逗号分隔字符串到List集合的格式转换

前言

最近在开发一个博客项目,有这么一个业务场景:

每篇文章,在发布时都可以设置一些标签,例如一篇和SpringBoot源码相关的文章可以有SpringSpringBoot源码等标签,文章和标签为多对多关系,存储在一张文章标签的关联表中,表中有id、blog_id、tag_id等字段。

在首页文章列表的检索sql中,不仅查询文章的基本信息,还想要直接把文章所绑定的标签名称查出来。这里是通过分组及关联查询关联出了标签数据,并通过GROUP_CONCAT函数封装为一个以逗号分割的字符串,类似这样:Spring,SpringBoot,源码

但在返回到前端之前,还需要将其处理为一个List集合或String数组,方便前端遍历展示。这里有两个方案:

  1. 查询出数据之后,遍历结果集,将每个字符串通过split方法转换为String数组。
  2. 利用Mybatis的TypeHandler机制,在结果集封装过程中,就自动进行数据格式的转换。

我是采用了第二种方式,下面就来分享一下 实现细节 以及 底层原理。

效果演示

先贴一下查询sql 这是一个简化后的版本,旨在说明查询的方式

sql 复制代码
SELECT b.uid, b.title, GROUP_CONCAT(t.name) tagNameList
FROM t_blog b
    LEFT JOIN t_blog_tag bt ON b.uid = bt.blog_uid AND bt.status = 1
    LEFT JOIN t_tag t ON bt.tag_uid = t.uid AND t.status = 1
WHERE b.status = 1
GROUP BY b.uid, b.title
ORDER BY b.create_time DESC

这里查询出来的tagNameList,只是一个以逗号分割的字符串,如下图

但是数据接收对象中,将其定义为了List集合

java 复制代码
@Data
public class BlogListVo implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 博客ID
     */
    private String uid;
    /**
     * 标题
     */
    private String title;
    /**
     * 标签名称
     */
    private List<String> tagNameList;
}

最后查询出来,可以看到 字符串自动封装到了List集合中:

这样,不用查询之后再手动处理,并且通用性较好,下面就分享一下实现细节。

实现细节

自定义TypeHandler

既然我们要用到TypeHandler,那么肯定要先定义一个自定义TypeHandler,所有的类型处理器都需要实现TypeHandler接口,重写其中的方法。

java 复制代码
/**
 * Mybatis类型处理器:将以逗号分割的字符串转化为List,使用场景:
 * 1、Mybatis-Plus实体类中,标注了@TableField注解的字段,设置typeHandler属性的值
 * 2、xml文件中,定义resultMap,在需要转换的字段映射中,设置typeHandler属性的值
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({List.class})
public class ConvertCommaSeparatedStrToListTypeHandler implements TypeHandler<List<String>> {
    @Override
    public void setParameter(PreparedStatement preparedStatement, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
        String items = StrUtil.join(",", strings);
        try {
            preparedStatement.setString(i, items);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public List<String> getResult(ResultSet resultSet, String s) throws SQLException {
        if (StrUtil.isNotBlank(resultSet.getString(s))) {
            return new ArrayList<>(Arrays.asList(resultSet.getString(s).split(",")));
        }
        return null;
    }

    @Override
    public List<String> getResult(ResultSet resultSet, int i) throws SQLException {
        if (StrUtil.isNotBlank(resultSet.getString(i))) {
            return new ArrayList<>(Arrays.asList(resultSet.getString(i).split(",")));
        }
        return null;
    }

    @Override
    public List<String> getResult(CallableStatement callableStatement, int i) throws SQLException {
        String items = callableStatement.getString(i);
        return StrUtil.isNotBlank(items) ? new ArrayList<>(Arrays.asList(items.split(","))) : null;
    }
}

配置TypeHandler

接下来需要在yml中配置一下TypeHanler的扫描路径,方便框架能找到这个类型处理器。

yml 复制代码
mybatis-plus:
  type-handlers-package: com.xb.blog.web.config.mybatis.typeHandler

注意:上面的配置方式是同时引入了Mybatis-plus之后的配置方式,如果你的项目是原生Mybatis,那么在Mybatis的配置类中,将这个处理器注册为Bean应该也可以,我没试过,可以自行尝试。

修改xml文件

上面就将TypeHandler配置好了,这里还需要修改一下xml文件,定义一个resultMap,在其中把需要转换的属性定义一下

xml 复制代码
<mapper namespace="com.xb.blog.web.dao.BlogDao">
    <resultMap id="BlogListVo" type="com.xb.blog.web.vo.BlogListVo">
        <result column="tagNameList" property="tagNameList" typeHandler="com.xb.blog.web.config.mybatis.typeHandler.ConvertCommaSeparatedStrToListTypeHandler"/>
    </resultMap>
    <select id="listBlog" resultMap="BlogListVo">
        SELECT b.uid, b.title, GROUP_CONCAT(t.name) tagNameList
        FROM t_blog b
            LEFT JOIN t_blog_tag bt ON b.uid = bt.blog_uid AND bt.status = 1
            LEFT JOIN t_tag t ON bt.tag_uid = t.uid AND t.status = 1
        WHERE b.status = 1
        GROUP BY b.uid, b.title
        ORDER BY b.create_time DESC
    </select>
</mapper>

这样,通过xml中sql查询时,就会自动适配自定义的TypeHandler,执行处理逻辑了:在本例中,就是将逗号分割的字符串封装为了List集合。

Mybatis-plus中使用

如果你要在Mybatis-plus中的实体类中使用,即不通过sql,直接调用Mybatis-plus封装好的方法查询数据,那么需要在实体类中,属性上面的@TableField注解中,设置typeHandler属性,效果就不演示了,可自行测试。示例如下:

java 复制代码
@Data
@TableName(value = "t_blog")
public class KeyPersonnelEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    //省略其它属性...
    
    @TableField(value = "tag_name_list", typeHandler = ConvertCommaSeparatedStrToListTypeHandler.class)
    private List<String> tagNameList;
}

底层原理

下面来解释一下底层原理,我们将带着两个问题出发:

  1. 在xml中定义的resultMap标签、result标签以及定义的typeHandler属性值,是怎么解析的?
  2. typeHandler是什么时候工作的?它里面的逻辑是什么时候触发的?

resultMap标签及属性解析

首先,在Mybatis中,负责解析xml文件的是XMLMapperBuilder,在它的parse方法中,完成了对一个xml文件中所有标签的解析。

(不了解的朋友 可以参考作者之前文章 Mybatis加载解析配置文件

parse方法中,首先获取了根标签/mapper,然后调用了configurationElement方法将其解析。

configurationElement 方法中,对xml文件中可能会配置的所有标签都进行了解析,其中就有我们要看的/mapper/resultMap标签,调用resultMapElements方法进行了解析。

resultMapElements 方法中,遍历了解析到的所有resultMap标签,经过多级调用,调用到了重载方法resultMapElement

在这个方法中,将解析resultMap标签的所有属性,封装到了一个ResultMapResolver对象中,封装时传入了一个成员变量builderAssistant,然后又调用了该对象的resolve方法。

resolve 方法中,将所有的属性都封装到了传入的成员变量builderAssistant

调用链到最后,会将ResultMap添加到全局配置对象Configuration对象中,方便后续在进行查询时,从该对象中获取到封装的ResultMap。

以上,可以明白,解析xml的过程中,将解析出来的resultMap标签以及其中的值,封装到了XMLMapperBuilder中的变量MapperBuilderAssistant builderAssistant中,最后通过该对象的resolve方法封装到了全局配置对象Configuration中。

结果集处理

上面完成了TypeHandler的解析,下面看一下它在哪里执行。

它的逻辑,肯定是在查询操作完成后,返回结果集之前,也就是封装结果集时执行的,在Mybatis中,结果集的封装工作由ResultSetHandler对象来完成,这是一个接口,我们主要来看一下它里边的handleResultSets方法

(不了解的朋友 可以参考作者之前文章 Mybatis 查询流程解析

这个接口只有一个实现类DefaultResultSetHandler,看一下他里边的源码:

遍历处理结果的过程中,调用了handleResultSet方法

调用了handleRowValues方法处理行数据

根据条件判断,执行不同的处理逻辑,这里是继续调用了handleRowValuesForSimpleResultMap方法

调用了getRowValue方法,这个方法返回了处理完毕的结果值

这个方法中,进行了多个方法调用,来对结果值进行处理,其中包含TypeHandler的处理逻辑在applyPropertyMappings方法中:

调用getPropertyMappingValue方法,获取属性映射的值。

到这里可以看到,最后一行,调用了typeHandler对象的getResult方法,这里就会调用到我们自定义的类型处理器。

也就是说 如果配置了类型处理器,最后返回的是经过类型处理器处理后的值。

总结

以上,就完成了基于自定义TypeHandler 实现从逗号分隔字符串到List集合的格式转换。

相关推荐
稚辉君.MCA_P8_Java38 分钟前
kafka解决了什么问题?mmap 和sendfile
java·spring boot·分布式·kafka·kubernetes
Lisonseekpan3 小时前
Spring Boot 中使用 Caffeine 缓存详解与案例
java·spring boot·后端·spring·缓存
Terio_my3 小时前
Spring Boot Web环境测试配置
spring boot
汤姆yu4 小时前
2025版基于springboot的美食食品商城系统
spring boot·后端·美食
韩立学长4 小时前
【开题答辩实录分享】以《走失人口系统档案的设计与实现》为例进行答辩实录分享
mysql·mybatis·springboot
kfepiza5 小时前
Spring 如何解决循环依赖 笔记251008
java·spring boot·spring
Arva .6 小时前
Spring Boot 配置文件
java·spring boot·后端
IT_Octopus6 小时前
https私人证书 PKIX path building failed 报错解决
java·spring boot·网络协议·https
风象南7 小时前
从RBAC到ABAC的进阶之路:基于jCasbin实现无侵入的SpringBoot权限校验
spring boot·后端
小蒜学长7 小时前
jsp基于JavaWeb的原色蛋糕商城的设计与实现(代码+数据库+LW)
java·开发语言·数据库·spring boot·后端