使用注解实现数据字典翻译
arminzheng Lv2

前言

在日常开发中查询单表的情况非常多。这时总会出现表里存的是编码(如部门编号),但却要返回对应的描述(如部门名称)。

通常一般思路是在 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();
}
  • 视图对象VO
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);
}
  • 实现了字典接口的 OrgService
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 来进行缓存。

(完)