Spring Data JPA使用原生SQL做复杂查询,结果封装到自定义实体

最近工作中使用到 Spring Data JPA 做持久层框架,个人感觉这个框架在做常规的查询时非常方便,在做定制化的高级查询时不够灵活。尤其是处理结果集的映射没有 MyBatis 方便。

本文概要:

1、spring-data-jpa 提供的 @Query 注解可以执行原生 SQL,其返回类型为 List<Object[]>
2、优雅地将 List<Object[]> 映射到我们自定义的实体中
3、使用到的知识点:自定义注解、反射

直接上代码

说明:此处的案例比较简单,只是为了演示此方案,复杂的查询使用方法一样。

实体类

1
2
3
4
5
6
7
8
9
10
@Entity
public class User {
@Id
private Integer id;
private String username;
private String name;
private Integer age;
private BigDecimal balance;
// getter、setter...
}

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import cn.huangxulin.jpa.utils.vo.Sign;

import java.math.BigDecimal;

/**
* 功能描述:
*
* @author hxulin
*/
public class UserVO {
private Integer id;
private String username;
private String name;
private Integer age;
private BigDecimal balance;

@Sign
public UserVO(Integer id, String username, String name, Integer age, BigDecimal balance) {
this.id = id;
this.username = username;
this.name = name;
this.age = age;
this.balance = balance;
}

@Sign(1)
public UserVO(Integer id, String username) {
this.id = id;
this.username = username;
}

@Sign(2)
public UserVO(String name, BigDecimal balance) {
this.name = name;
this.balance = balance;
}

// getter、setter...
}

@Sign 注解用于区分不同的构造器。后面使用到 ModelMap.mapping(List<Object[]> list, Class<T> clazz, int sign) 方法做结果集映射,第三个参数 sign 传入此处注解定义的值。

注意点:
1、VO 对象构造器中的参数类型、数量和数据库查询结果集的类型、列数一定要保持一致。参数名称和列名可以不同。
2、自定义实体类 VO 的字段尽量使用包装类型(IntegerLong…),不使用基本类型(intlong…)。当数据库查询的字段为 NULL 时,映射到 intlong 等基本类型会报错。

DAO 层 JPA 处理接口

1
2
3
4
5
6
7
8
9
10
11
12
public interface UserRepository extends JpaRepository<User, Integer> {

@Query(value = "SELECT * FROM user", nativeQuery = true)
List<Object[]> query();

@Query(value = "SELECT id, username FROM user", nativeQuery = true)
List<Object[]> query1();

@Query(value = "SELECT name, balance FROM user", nativeQuery = true)
List<Object[]> query2();

}

此处直接调用 JPA 接口方法,将会返回 List<Object[]> 类型,格式如下,非常不方便我们进行数据处理。

list-object-log.png

Service 层方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public List<UserVO> query() {
List<Object[]> list = userRepository.query();
return ModelMap.mapping(list, UserVO.class);
}

public List<UserVO> query1() {
List<Object[]> list = userRepository.query1();
// 第三个参数 1 和 @Sign 注解标记在 UserVO 构造器上的值对应
return ModelMap.mapping(list, UserVO.class, 1);
}

public List<UserVO> query2() {
List<Object[]> list = userRepository.query2();
// 第三个参数 2 和 @Sign 注解标记在 UserVO 构造器上的值对应
return ModelMap.mapping(list, UserVO.class, 2);
}

测试类

1
2
3
4
5
6
@Test
public void testQuery() {
List<UserVO> result = userService.query2();
String json = JSONUtils.toJson(result);
logger.info(json);
}

test-result.png

根据构造器的自定义参数映射后的结果

实现数据映射的两个核心类

@Sign 注解:标记构造器,区分不同的结果集映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 功能描述: 自定义标记注解,贴在VO的构造器上,用于JPA查询结果的封装
*
* @author hxulin
*/
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
int value() default 0;
}

ModelMap:完成结果集映射

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
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;

/**
* 功能描述: JPA查询结果映射工具类
*
* @author hxulin
*/
public final class ModelMap {

private ModelMap() {

}

/**
* 说明: 将JPA查询的List<Object[]>结果集封装到自定义对象中
*
* @param list 待处理数据
* @param clazz 待映射对象类型,该对象的构造方法需要结合@Sign注解使用
* @param sign 待映射对象构造器上的注解值,用于匹配不同的结果集映射
* @param <T> 待映射对象泛型
* @return 映射后的结果集
*
*/
@SuppressWarnings("unchecked")
public static <T> List<T> mapping(List<Object[]> list, Class<T> clazz, int sign) {
if (list != null) {
Constructor<?>[] constructors = clazz.getConstructors();
for (Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(Sign.class)) {
if (sign == constructor.getAnnotation(Sign.class).value()) {
List<Object> result = new ArrayList<>(list.size());
try {
for (Object[] data : list) {
result.add(constructor.newInstance(data));
}
return (List<T>) result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
throw new RuntimeException("\"" + clazz.getName() + "\" constructor need use annotation \"" + Sign.class.getName() + "\"");
}
return null;
}

/**
* 说明: 将JPA查询的List<Object[]>结果集封装到自定义对象中
* 为默认的注解值提供一个便捷的方法,不用传递注解参数0
*/
public static <T> List<T> mapping(List<Object[]> list, Class<T> clazz) {
return mapping(list, clazz, 0);
}

}

完整项目地址:https://github.com/hxulin/whampoa/tree/master/spring-data-jpa

参考文档:

Spring Data Jpa框架自定义查询语句返回自定义实体的解决方案

Spring Data JPA 查询结果返回至自定义实体

(完)