用习惯了mybatis及mybatisplus,刚开始拿起spring-data-jpa,其无法局部更新、原生sql查询返回非JavaBean的List<Map<String, Object>>、分页无法返回自定义对象等等的问题着实差点没让我拔出我的30米大砍刀。不过咱吐槽归吐槽,遇到问题,想办法一一去解决问题即可。

一、让JPA支持局部更新

jpa中的新增与更新都是调用的save()方法,以是否携带id做区分,无奈的是它对局部变量更新并未做何处理,依旧需要我们自己动手。

  • 解决方法:
    BeanUtils复制对象,BeanUtils中的构造方法属性中可以通过传入更新时忽略的属性值来实现选择性复制原对象的字段。更新部分字段时,我们仅需要传入复制后的字段即可。

  • 解析和实现:
    (1)查询出待更新对象的原有信息
    (2)通过传入的更新的象去复制产生一个新对象,其中新对象中为null的字段不需要更新。
    (3)执行更新操作,操作对象时步骤2得出的复制对象。

  • 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional
public Long update(TestCommand command) {
Optional<TestEntity> optional = testRepository.findById(command.getId());
if (!optional.isPresent()) {
return 0L;
}
Appointment target = optional.get();
// 复制对象
BeanUtils.copyProperties(command, target, BeanUtil.getNullProperty(command));

target.setOther(command.getOther());
TestEntity result = testRepository.save(target);
if (result == null) {
return 0L;
}
return result.getId();
}

代码中的getNullProperty作为通用工具类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 获取空属性
*/
public static String[] getNullProperty(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();

Set<String> emptyNames = new HashSet<String>();
for (PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) {
emptyNames.add(pd.getName());
}
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}

二、让JPA原生SQL查询返回ListBean

jpa使用@Query注解来做原生sql查询,一般是这样的画风:

1
2
@Query(value = "SELECT id, name FROM xxx WHERE type = :type", nativeQuery = true)
List<Map<String, Object>> findTest(@Param("type") String type);

其返回值List<Map<String, Object>>怎么看怎么不舒服,那么如何将之转换为我们想要的List<JavaBean>呢?先直接看看经封装转换后的返回效果:

1
2
3
4
// 正常原生SQL查询
List<Map<String, Object>> mapList = testRepository.findTest("testType");
// 工具类转换
List<TestResult> beanList = BeanUtil.mapToBean(mapList, TestResult.class);

工具类代码:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* 根据List<Map<String, Object>>数据转换为List<JavaBean>数据
*/
public static <T> List<T> mapToBean(List<Map<String, Object>> datas, Class<T> beanClass) {
// 返回数据集合
List<T> list = null;
// 对象字段名称
String fieldName = "";
// 对象方法名称
String methodname = "";
// 对象方法需要赋的值
Object methodsetvalue = "";
try {
list = new ArrayList<T>();
// 得到对象所有字段
Field fields[] = beanClass.getDeclaredFields();
// 遍历数据
for (Map<String, Object> mapdata : datas) {
// 创建一个泛型类型实例
T t = beanClass.newInstance();
// 遍历所有字段,对应配置好的字段并赋值
for (Field field : fields) {
// 获取字段名称
fieldName = field.getName();
if ("serialVersionUID".equals(fieldName)) {
continue;
}
// 拼接set方法
methodname = "set" + StringUtils.capitalize(fieldName);
// 获取data里的对应值
methodsetvalue = mapdata.get(fieldName);
// 赋值给字段
Method m = beanClass.getDeclaredMethod(methodname, field.getType());
// Integer
if (field.getType() == Integer.class) {
if (methodsetvalue == null) {
} else {
m.invoke(t, Integer.parseInt(methodsetvalue.toString()));
}
}
// Long
else if (field.getType() == Long.class) {
if (methodsetvalue == null) {
} else {
m.invoke(t, Long.parseLong(methodsetvalue.toString()));
}
}
// Boolean
else if (field.getType() == Boolean.class) {
if (methodsetvalue == null) {
m.invoke(t, Boolean.FALSE);
} else {
m.invoke(t, Boolean.parseBoolean(methodsetvalue.toString()));
}
}
// Byte
else if (field.getType() == Byte.class) {
if (methodsetvalue == null) {
m.invoke(t, null);
} else {
m.invoke(t, Byte.parseByte(methodsetvalue.toString()));
}
}
// Short
else if (field.getType() == Short.class) {
if (methodsetvalue == null) {
m.invoke(t, null);
} else {
m.invoke(t, Short.parseShort(methodsetvalue.toString()));
}
}
// Float
else if (field.getType() == Float.class) {
if (methodsetvalue == null) {
m.invoke(t, null);
} else {
m.invoke(t, Float.parseFloat(methodsetvalue.toString()));
}
}
// Double
else if (field.getType() == Double.class) {
if (methodsetvalue == null) {
m.invoke(t, null);
} else {
m.invoke(t, Double.parseDouble(methodsetvalue.toString()));
}
} else {
m.invoke(t, methodsetvalue);
}

}
// 存入返回列表
list.add(t);
}
} catch (InstantiationException e) {
try {
throw new Exception("创建beanClass实例异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (IllegalAccessException e) {
try {
throw new Exception("创建beanClass实例异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (SecurityException e) {
try {
throw new Exception("获取[" + fieldName + "] getter setter 方法异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (NoSuchMethodException e) {
try {
throw new Exception("获取[" + fieldName + "] getter setter 方法异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (IllegalArgumentException e) {
try {
throw new Exception("[" + methodname + "] 方法赋值异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (InvocationTargetException e) {
try {
throw new Exception("[" + methodname + "] 方法赋值异常", e);
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 返回
return list;
}

三、让JPA分页查询返回自定义对象

JPA分页查询的方式有多种,以最常见的Page<TestEntity> page = testRepository.findAll(specification, pageable);为例,其返回值的泛型即为实体。如果想返回自定对象怎么办?不好意思,在不写SQL的情况下JPA目前并没有相应的支持,只能自己去动手丰衣足食。以Jave8的语法为例,我们可以这样处理:

  1. 首先我们需要自己再封装一个Page对象,用来取代原生返回的Page对象

    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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    package com.xfktech.nurse.appointment.nursing.common.po;

    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageImpl;

    import java.util.List;

    @Getter
    @Setter
    @NoArgsConstructor
    public class PageResponse<T> {

    /**
    * 页码
    */
    private int pageNum;

    /**
    * 页数
    */
    private int pageSize;

    /**
    * 总页数
    */
    private int totalPage;

    /**
    * 总条数
    */
    private long totalSize;

    /**
    * 是否最后一页
    */
    private boolean last;

    /**
    * 数据列表
    */
    private List<T> content;

    public static <T> PageResponse<T> of(Page<T> page) {
    return new PageResponse<>((PageImpl<T>) page);
    }

    private PageResponse(PageImpl<T> page) {
    this.setPageNum(page.getPageable().getPageNumber() + 1);
    this.setPageSize(page.getPageable().getPageSize());
    this.setTotalPage(page.getTotalPages());
    this.setTotalSize(page.getTotalElements());
    this.setLast(page.isLast());
    this.setContent(page.getContent());
    }
    }
  2. 为接口返回值新建vo转换类,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class TestGenerator {
    // TestResult即我们需返回的自定义对象
    public static TestResult test(TestEntity entity) {
    /**
    * 此处与以下代码等同:
    * TestResult result = new TestResult();
    * result.setId(entity.getId());
    * ...
    * return result;
    */
    return TestResult.builder()
    .id(entity.getId())
    .name(entity.getCreateTime())
    .build();
    }
    }
  3. 最后,仅需要在如常调用原生分页Page<TestEntity> page = testRepository.findAll(specification, pageable);之后,将page做以下转换即可:

    1
    PageResponse<TestResult> result = PageResponse.of(page.map(TestGenerator::test));

如需往test()方法额外传参,那么也可以选择这样写:

1
PageResponse<TestResult> result = PageResponse.of(page.map(obj -> AlarmGenerator.common(test, "xxx")));

完整分页示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
@Transactional(readOnly = true)
public PageResponse<TestResult> list(TestQuery query) {
Pageable pageable = PageRequest.of(query.getPageNum(), query.getPageSize());
Specification<TestEntity> specification = (Specification<TestEntity>) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> list = new ArrayList<>();
// 未删除
list.add(criteriaBuilder.equal(root.get("isDelete"), false));
// 排序
criteriaQuery.orderBy(criteriaBuilder.desc(root.get("createTime")));
return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
};
Page<TestEntity> page = testRepository.findAll(specification, pageable);
// 转vo
PageResponse<TestResult> result = PageResponse.of(page.map(TestGenerator :: test));

return result;
}

The end.

参考资料: