前言
在日常开发中查询单表的情况非常多。这时总会出现表里存的是编码(如部门编号),但却要返回对应的描述(如部门名称)。
通常一般思路是在 Service 进行关联查询或依赖组件完成。比如 Mybatis 中用 join 语句将 sql 写死,比如 JPA 中在实体类属性字段加上@ManyToOne注解,直接将对象组合起来。
1 2 3 4
| 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再加一个属性,但考虑到我们的业务层或许也需要该字段,就添个属性用来存放)。接下来就开工。
实现
注解
- @Dict 包含填充目标属性,和调用的service
1 2 3 4 5 6 7
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented public @interface Dict { String target(); String service(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @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(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @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); } }
|
- 为了能进行统一调用,写了一个 DictService 字典接口
1 2 3
| public interface DictService { Object getValue(String key); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @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(); } }
|
- 获取 Bean 的工具类,使用了 Spring 的 ApplicationContextAware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @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); } }
|
1 2 3 4 5 6 7 8 9
| ... 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表存放:gender | 性别
B表存放:gender | 1 | 男 、 gender | 2 | 女 ;
(全部放在一张表也行,可根据数据复杂度而定)
最后通过单表的字段值 1 来进行数据字典表查找。有时候数据字典会有很多其他别名,如标准码、标准代码,多见于专业领域。
由于数据字典表的特性,在写入之后,很少会去修改,非常适合结合Redis来进行缓存,提高查询数据。
我们可以在对应数据字典的Service层接入 Spring Cache + Redis 来进行缓存。
(完)