系统接入了多个支付渠道:招行、建行、工行、农行等用于收款,支付时需要很多的参数,如:签名、加密相关等。每个支付渠道的商户配置参数都不同,共同点是存在商户号、商户名字等字段。当前需要实现的是在 一个 HTTP 接口中实现各个渠道商户信息详情查询功能。
先来说当前库里存在的表结构设计,采用的是 “父子表关联” 形式,父表(mch_info) 中存放各个渠道公用字段,各个子表(xxx_mch_info) 中存在渠道特有字段,当渠道子表还存在子表时,继续用 mch_id
进行关联。如下图所示:
如此设计原因有几点: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,不用新增逻辑,就达到了复用的效果,也遵循了开闭原则。总的来说,是一次比较恰当的实践。