基于 Mybatis discriminator 语法与父子表、实体继承结合的一次实践

系统接入了多个支付渠道:招行、建行、工行、农行等用于收款,支付时需要很多的参数,如:签名、加密相关等。每个支付渠道的商户配置参数都不同,共同点是存在商户号、商户名字等字段。当前需要实现的是在 一个 HTTP 接口中实现各个渠道商户信息详情查询功能

先来说当前库里存在的表结构设计,采用的是 “父子表关联” 形式,父表(mch_info) 中存放各个渠道公用字段,各个子表(xxx_mch_info) 中存在渠道特有字段,当渠道子表还存在子表时,继续用 mch_id 进行关联。如下图所示:

image.png

如此设计原因有几点:1、符合数据库表设计,遵循第三范式 2、数据量不大,连表查询即使频繁也不会影响性能。
再来看 Java 代码这边,响应 Vo 实体类的设计与数据库保持一致,采用继承,公共字段放在父类,子类继承下去并具有自己的属性。如下:

// 父类
public class QueryDetailRespVO {
     private String mchCode
     private String mchName;
     private String channelCode;
     // .... 其他的公共字段
}
// 招行子类
public class QueryCmbDetailRespVO extends QueryDetailRespVO {
    private String appId;
    private String appSecret;
    // .... 其他的字段
}
// 其他渠道同理
复制代码

刚开始写这个需求时,采用最一般方法,老老实实地在每个 xxx_mch_info 的 Mapper 接口中新建了 queryDetail() 方法。写了几个之后发现各个渠道的大部分逻辑都是一样的,如下:

QueryXxxDetailRespVO queryMchDetail(@Param("id") Integer id);
复制代码

XML 文件如下:

<select id="queryMchDetail" resultMap="XxxDetailResult">
    select m.id as mch_info_id,
    m.mch_code,m.mch_name,
    m.channel_code, xmi.*
    from xxx_mch_info xmi ,mch_info m
    where 1 = 1
    and m.id = xmi.mch_info_id
    <if test="id != null">
        and m.id = ${id}
    </if>
</select>
复制代码

总结一下,不同的渠道,变化的东西只有两点,如下:

  • xxx_mch_info:需要查询的字表表名不一样
  • XxxDetailResult:对结果的反序列化处理不一样

说到底,只要搞定这两点,这些方法都可以合并成一个。第一点 xxx_mch_info 动态表名是好实现的,一个渠道代码(内部规定的渠道标识)对应一个子表表名,只需要做一层映射,将查出来的表名传入方法即可。

修改方法签名和 XML Mapper 文件如下,注意,利用多态需要把返回值设置为父类,具体子类由 Mybatis 创建。

QueryDetailRespVO queryMchDetail(@Param("id") Integer id, @Param("tbName") String tn);
复制代码

Mapper 文件修改如下,当在 SQL 语句中使用 字符串值替换,而不是 预编译 时,statementType 必须设置为 STATEMENT。详情见 官网描述

<select id="queryMchDetail" resultMap="XxxDetailResult" statementType="STATEMENT">
    select m.id as mch_info_id,
    m.mch_code,m.mch_name,
    m.channel_code, xmi.*
    from ${tbName} xmi ,mch_info m
    where 1 = 1
    and m.id = xmi.mch_info_id
    <if test="id != null">
        and m.id = ${id}
    </if>
</select>
复制代码

第二点,稍微复杂,好在 MyBatis 也提供 动态决定 resultMap 的语法,discriminator 标签,详见 官网。这里跳过解释,直接来看用法:

<!-- 父结果集映射器  -->
<resultMap id="DetailResult" type="xxx.QueryDetailRespVO">
    <id column="mch_info_id" property="id"/>
    <result column="mch_code" property="mchCode"/>
    <result column="mch_name" property="mchName"/>
    <result column="channel_code" property="channelCode"/>

    <discriminator javaType="String" column="channel_code">
        <!-- channel_code 字段值为 CCB 时,使用 CcbDetail 结果映射器 -->
        <case value="CCB" resultMap="CcbDetail"/>
        <case value="CMB" resultMap="CmbDetail"/>
        <!-- 其他的渠道 -->
    </discriminator>
</resultMap>

<!-- 使用 extends 继承 DetailResult 中配置的公共属性  -->
<!-- 只需要配置子表中的特有字段即可  -->
<resultMap id="CmbDetail" type="xxx.QueryCmbDetailRespVO" extends="DetailResult">
    <result column="app_id" property="appId"/>
    <result column="app_secret" property="appSecret"/>
</resultMap>

<resultMap id="CcbDetail" type="xxx.QueryCcbDetailRespVO" extends="DetailResult">
    <result column="branch_id" property="branchId"/>
    <result column="operator" property="operator"/>
</resultMap>

<select id="queryMchDetail" resultMap="DetailResult" statementType="STATEMENT">
    select m.id as mch_info_id,
    m.mch_code,m.mch_name,
    m.channel_code, xmi.*
    from ${tbName} xmi ,mch_info m
    where 1 = 1
    and m.id = xmi.mch_info_id
    <if test="id != null">
        and m.id = ${id}
    </if>
</select>
复制代码

不难理解,queryMchDetail 方法根据 ID 从 父类+子表 中查询出唯一的一条记录,Mybatis 根据 DetailResult 解析这条记录。当该记录中的 channel_code 字段值为 CCB 时,则使用 CcbDetail 这个结果映射器(discriminator 标签实现)。至于那些公用字段,子表的结果映射器会通过 extends 关键字从父映射器中继承下来。其他渠道同理。

回顾上文,其实是将 Mybatis 的一些高级特性和具体的表设计相结合,实现了多态这个特性。基于此,假如后续有新增的渠道,第一步按照规范新建子表关联父表,接着再新增响应实体类和对应的 ResultMap,不用新增逻辑,就达到了复用的效果,也遵循了开闭原则。总的来说,是一次比较恰当的实践。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享