轮子---ElasticSearch的Nested结果到实体的映射

背景

项目使用了ES来替换MySQL的查询,然而在ES的实体定义过程中,我们选择了一个索引多Nested的形式来存储数据,这样就带来了一个结果就是查询出来的ES结果被打散到各个Nested的集合中去了。同时为了降低改造的耦合度,和前端的接口定义并没有修改,这就需要将层级的ES结果再打散到扁平的实体中去。因为映射的字段不同,甚至于映射的层级也不同,BeanUtils#copyProperties()方法并不适用,同时由于我们虽然使用了Nested类型,但是Nested下的List中的元素最多只有一个(业务上可以限定住)。因此这里简单造了个轮子来实现该方法。

实体VO

如下是ES的model,其中的Student和Course都是Nested类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Document(indexName = "index",type = "type")
public class BasicDataResult implements Serializable {
private static final long serialVersionUID = 1L;
@Field(type = FieldType.Nested)
private List<Student> student;
@Field(type = FieldType.Nested)
private List<Course> course;
}

class Student{
private Integer id;
private Integer name;
}

class Course{
private Integer id;
private Integer name;
}

那么其查询结果是形如下面的json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"student": [
{
"id": 1,
"name": "echo"
}
],
"course": [
{
"id": 1,
"name": "java"
}
]
}

而我们正常和前端约定好的返回结果可能是这个样子

1
2
3
4
5
6
{
"studentId":1,
"studentName":"echo",
"courseId":1,
"courseName":"java"
}

显然,其对应的实体就是酱样

1
2
3
4
5
6
public class Info{
private Integer studentId;
private Integer courseId;
private String studentName;
private String courseName;
}

实现思路

参照BeanUtils#copyProperties(),以及Mybatis的ResultMap的实现方式,显然反射是一个很奈斯的选项。

代码

首先,既然是不同名称的字段的对照规则,自然需要有一张对照表来告诉机器字段间的对照关系。这样的对照表很自然的就想到了map来存储。这里定义了一个抽象父类AbstractConvertRule

1
2
3
public abstract class AbstractConvertRule {
public abstract Map<String,String> getConvertMap();
}

又因为这里的需求是从ES的Nested结构到扁平实体,因此又定义了一个抽象父类ElasticNestedConvertRule来提供一些默认的实现。

1
2
3
4
5
6
7
8
public abstract class ElasticNestedConvertRule extends AbstractConvertRule {
protected static String getStudent() {
return "Student.";
}
protected static String getCourse() {
return "Course.";
}
}

ok,下面就是最终的映射实体类了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* key-value的关系和BeanUtils#copyProperties是反着的,emmm,好像不太友好
*/
public class BasicDataToInfoRule extends ElasticNestedConvertRule{
/** 转换规则,key是待转换的,value是es的*/
private static Map<String,String> map = new HashMap<>();
// 父类提供的默认实现
private static final String STUDENT = getStudent();
private static final String COURSE = getCourse();
// key值是Info的字段,value是常量+该常量的实体中的字段
static {
map.put("studentId",STUDENT + "id");
map.put("courseId",COURSE + "id");
map.put("studentName",STUDENT + "name");
map.put("courseName",COURSE + "name");
}
@Override
public Map<String,String> getConvertMap() {
return map;
}
}

既然对照关系出来了,剩下的就是通过反射来将A实体中的值映射到B实体中去了,下面是转换的代码

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
/**
* 将es查询出得bean转换成新的VO对象
* @param setBean 待转换的实体
* @param getBean es查询的实体
* @param convertData 转换规则,key是待转换的,value是es的
*/
public static void convertData(Object setBean, Object getBean, Map<String,String> convertData) throws Exception {
Field[] fields = setBean.getClass().getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if(convertData.containsKey(fieldName)){
Object value = null;
String[] split = convertData.get(fieldName).split("\\.");
if(split.length != 2){
throw new InvalidPropertiesFormatException("convertData中,key值为:"+fieldName+"的value值不合法,请检查");
}
// Step1:先从BaseDataResult中取得具体的Bean
Object tableBean = dynamicGet(getBean, StringUtils.toLowerCaseFirstOne(split[0]));
if(tableBean instanceof ArrayList){
if(((ArrayList) tableBean).isEmpty()){
continue;
}
List<?> list = castObjectToList(tableBean);
Object fieldBean = list.get(0);
// Step2:根据Step1中取得的Bean和具体的字段取值
value = dynamicGet(fieldBean,split[1]);
}else {
value = tableBean;
}
// Step3: 将第二步拿到的值塞入待转换的实体中
dynamicSet(setBean,fieldName,value);
}
}
}

下面列了一些#convertDate中用到的私有方法

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
public static Object dynamicGet(Object obj,String propertyName) throws NoSuchFieldException,IllegalAccessException{
try {
Field field = obj.getClass().getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException e) {
throw new NoSuchFieldException(String.format("实体:%s中不包含:%s字段,请检查对照规则",obj,propertyName));
} catch (IllegalAccessException e) {
throw new IllegalAccessException(String.format("实体:%s中%s字段的不可见,请检查",obj,propertyName));
}
}

/**
* 这里如果对照的实体类型不同可能需要进行特殊处理,如从Integer-->Long,Boolean-->Integer等
*/
public static void dynamicSet(Object obj,String propertyName,Object value) throws NoSuchFieldException,IllegalAccessException{
try {
Field field = obj.getClass().getDeclaredField(propertyName);
// 待塞入的值是Long而value值是Integer类型
if(field.getType() == Long.class && value instanceof Integer){
value = ((Integer) value).longValue();
}else if(field.getType() == Integer.class && value instanceof Byte){
value = ((Byte) value).intValue();
}
field.setAccessible(true);
field.set(obj,value);
} catch (NoSuchFieldException e) {
throw new NoSuchFieldException(String.format("实体:%s中不包含:%s字段,请检查对照规则",obj,propertyName));
} catch (IllegalAccessException e) {
throw new IllegalAccessException(String.format("实体:%s中%s字段的不可见,请检查",obj,propertyName));
}
}

private static <T extends List<?>> T castObjectToList(Object obj){
return (T) obj;
}

至此,工具类就算完工了,剩下的就是调用了,其调用方式就喝BeanUtils#copyProperties差不多了,只不过注意是参数反过来了

1
2
3
4
BasicDataToInfoRule rule = new BasicDataToInfoRule();
Map<String,String> convertMap = rule.getConvertMap();
Info info = new Info();
BaseConvert.convertData(info,basicDataResult,convertMap);

后记

再之后的需求中,又遇到了将带下划线的实体映射到驼峰的实体中去,不过好在本次是从一个扁平的实体到另一个扁平的实体中,因此继承抽象规则AbstractConvertRule类覆盖其#getConvertMap

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UnderLineToCamelRule extends AbstractConvertRule{
private static Map<String,String> map = new HashMap<>();

static{
map.put("s_id","sid");
map.put("s_name","sName");
}

@Override
public Map<String, String> getConvertMap() {
return map;
}
}

而后在BaseConvert的工具类中增加了对扁平实体的对照方法#convertVO,因为不需要处理层级关系,代码简单很多

1
2
3
4
5
6
7
8
9
10
public static void convertVO(Object setBean,Object getBean,Map<String,String> convertMap) throws Exception{
Field[] fields = setBean.getClass().getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if(convertMap.containsKey(fieldName)) {
Object value = dynamicGet(getBean, convertMap.get(fieldName));
dynamicSet(setBean,fieldName,value);
}
}
}

感想

  • 平时在实现一个工具类的过程中,最好提供足够优秀的可扩展性
  • 良好设计模式的重要性

轮子---ElasticSearch的Nested结果到实体的映射
http://yuyangblog.cn/2019/09/13/轮子-ElasticSearch的Nested结果到实体的映射/
Aŭtoro
于洋
Postigita
September 13, 2019
Lizenta