在日常开发中查询单表的情况非常多。这时总会出现表里存的是编码(如部门编号),但却要返回对应的描述(如部门名称)。
通常一般思路是在 Service 进行关联查询或依赖组件完成。比如 Mybatis 中用 join 语句将 sql 写死,比如 JPA 中在实体类属性字段加上@ManyToOne注解,直接将对象组合起来。
private String orgId; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "orgId", insertable = false, updatable = false) private BaseOrg baseOrg;
上面的方式固然简单直接,但是我觉得还不够快,而且过度依赖组件在以后的修改中也会比较麻烦,接下来就由我来提供一种新的思路。
无论是列表还是单个查询,本质上是先找到编码,再去找对应描述,首要条件就是:顺序不能颠倒,我们不能进行预判。所以我们的任务就像一条流水线一样,得到数据进行查询,再返回填充。如果是列表,那就遍历一遍,时间复杂度O(n)。
而这样一个过程其实是非常模范的,容易提炼出来。我起先的思路是结合Spring的切面来做,可深度考虑后发现切面只能针对方法的调用,而方法的返回值有很多种,单个对象、List以及IPage分页等。放在 set() 方法上也没有办法得到该 set() 对应的实体再填充。后面转换思路写为工具类在所需要的地方进行调用,一切都简单了不少。比如加入到 MP 的分页转换过程中( IPage <PO>
to IPage <VO>
)。
工具类的思路确定了。我们剩下还需要的。1是查询对应的编码所需要的单表查询Service,2是填充的属性名称(如果是Json动态添加一个JsonElement就不需要在VO再加一个属性,但考虑到我们的业务层或许也需要该字段,就添个属性用来存放)。接下来就开工。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented public @interface Dict { String target(); String service(); }
@Data @NoArgsConstructor @ApiModel(value = "员工VO", description = "EmployeeVO") public class EmployeeVO implements Serializable { /** 名称 */ @ApiModelProperty("名称") private String name; /** 编号 */ @ApiModelProperty("编号") private String number; /** 所属机构代码 */ @ApiModelProperty("所属机构代码") @Dict(target = "orgName", service = "baseOrgService") private String orgCode; /** 所属机构 */ @ApiModelProperty("所属机构") private String orgName; public EmployeeVO(EmployeePO po) { this.name = po.getName(); this.number = po.getNumber(); this.orgCode = po.getOrgCode(); } }
@Slf4j public class BeanHelpUtils { /// 主要代码 ⬇️ public static <T> void translation(T t) throws IntrospectionException, InvocationTargetException, IllegalAccessException { Field[] fields = t.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Dict.class)) { String target = field.getAnnotation(Dict.class).target(); String service = field.getAnnotation(Dict.class).service(); DictService dictService = SpringContextUtil.getBean(service, DictService.class); if (dictService != null) { PropertyDescriptor source = new PropertyDescriptor(field.getName(), t.getClass()); Object invoke = source.getReadMethod().invoke(t); if (invoke instanceof String) { Object result = dictService.getValue((String) invoke); PropertyDescriptor targetResult = new PropertyDescriptor(target, t.getClass()); targetResult.getWriteMethod().invoke(t, result); } } } } } /// 主要代码 ⬆️ public static <T> void translation(List<T> collect) { for (T t : collect) { try { translation(t); } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) { if (log.isInfoEnabled()) log.info(e.getMessage()); e.printStackTrace(); } } } /** 分页复制 */ public static <T, E> IPage<T> pageTransform(IPage<E> page, Function<E, T> sup) { if (page == null || page.getRecords() == null) return null; List<T> collect = page.getRecords().stream().map(sup).collect(Collectors.toList()); translation(collect); return new Page<T>(page.getCurrent(), page.getSize(), page.getTotal()).setRecords(collect); } }
public interface DictService { Object getValue(String key); }
@Service public class BaseOrgService extends ServiceImpl<BaseOrgMapper, BaseOrgPO> implements DictService { @Override public Object getValue(String orgCode) { BaseOrgPO po = baseMapper.selectOne( new QueryWrapper<BaseOrgPO>() .lambda() .eq(BaseOrgPO::getOrgCode, orgCode) .last("LIMIT 1")); if (po == null) return null; return po.getOrgName(); } }
@Configuration public class SpringContextUtil implements ApplicationContextAware { public static ApplicationContext applicationContext; @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { SpringContextUtil.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } public static String getProperty(String path) { return applicationContext.getEnvironment().getProperty(path); } public static Object getBean(String name) throws BeansException { if (applicationContext == null) return null; return applicationContext.getBean(name); } public static <T> T getBean(String name, Class<T> requiredType) throws BeansException { if (applicationContext == null) return null; return applicationContext.getBean(name, requiredType); } }
// 调用分页转换,自动翻译 ... IPage<EmployeePO> poPage = employeeMapper.selectPage(page, new QueryWrapper<EmployeePO>().lambda() .eq(... return BeanHelpUtils.pageTransform(poPage, EmployeeVO::new); // 或直接调用翻译 List<EmployeeVO> records = poPage.getRecords(); BeanHelpUtils.translation(records); return records;
由于我一开始提到的是数据字典,其实数据字典通常是一张或者两张表,用来存放编码和对应值,如:
A表存放:
key | value |
---|---|
gender | 性别 |
B表存放:
key | value | display |
---|---|---|
gender | 1 | 男 |
gender | 2 | 女 |
(只留一张 B 表也行,可根据数据复杂度而定)
最后通过单表的 value 值和名称 gender 来进行数据字典表查找。
有时候数据字典会叫别的名字,如标准码、标准代码,多见于专业领域。
由于数据字典表的特性,在写入之后,很少会去修改,非常适合结合Redis来进行缓存,提高查询数据。
我们可以在对应数据字典的 Service 层接入 Spring Cache + Redis 来缓存。
(完)