环境
开发框架使用的是springboot,持久框架使用的是spring data jpa+QueryDSL,具体版本参见下面的pom文件。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>rm.lesi</groupId>
<artifactId>mms-server</artifactId>
<version>0.0.1</version>
<name>mms-server</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--QueryDSL支持-->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--swagger2支持-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.23</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.23</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
问题
在使用spring data jpa+QueryDSL开发过程中发现,当多表查询返回结果不使用DTO进行封装的话只能使用Projections.map进行封装,保证返回的结果集是key-value形式,而不是object[],但是在实际中发现返回的结果集里key的形式是QEntity的名字.字段名(如:mem.birthDate)。
查询部分的代码如下:
public Map<String, Object> findAllMember(Pageable pageable) {
QMemberEntity qMember = new QMemberEntity("mem");
JPAQuery query = jpaQueryFactory.select(
Projections.map(
qMember.memberGuid.as("memberGuid"),
qMember.birthDate,
qMember.guardianSShip,
qMember.realName,
qMember.nickName))
.from(qMember)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
Map<String, Object> result = new HashMap<>(5);
result.put("total", query.fetchCount());
result.put("data",query.fetch());
result.put("pageSize", pageable.getPageSize());
result.put("pageNumber", pageable.getPageNumber());
result.put("offset", pageable.getOffset());
return result;
}
返回的执行结果如下:
{
"code": "200",
"msg": "ok",
"data": {
"pageSize": 10,
"total": 196,
"pageNumber": 0,
"data": [
{
"mem.birthDate": "2014-03-13",
"mem.guardianSShip": 12,
"mem.realName": "asddd",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "c0091fe1-6faa-473d-9428-cf9f02fda899"
},
{
"mem.birthDate": "2014-08-06",
"mem.guardianSShip": 12,
"mem.realName": "asddd1",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "35406c3e-da06-426c-998b-c41e59c38941"
},
{
"mem.birthDate": "2019-05-01",
"mem.guardianSShip": 12,
"mem.realName": "asddd3",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "94d04f7c-752a-4451-babf-a53c736de9da"
},
{
"mem.birthDate": "2009-05-08",
"mem.guardianSShip": 13,
"mem.realName": "测试3",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "926f53d1-aec8-4172-9c74-c4bbe7e0f92c"
},
{
"mem.birthDate": "2015-12-23",
"mem.guardianSShip": 13,
"mem.realName": "asddd4",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "31c5edc3-d1c6-4d33-a9ba-684fa77a7051"
},
{
"mem.birthDate": "2014-10-11",
"mem.guardianSShip": 12,
"mem.realName": "asddd5",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "4cf95517-5ff0-4c1a-98a4-019f2ebac1af"
},
{
"mem.birthDate": "2011-12-31",
"mem.guardianSShip": 13,
"mem.realName": "asddd6",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "f9ef258e-05f7-434b-9a35-928004bd129d"
},
{
"mem.birthDate": "2016-05-25",
"mem.guardianSShip": 0,
"mem.realName": "测试2",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "7c5ac178-9903-4c18-88d2-80310d77839d"
},
{
"mem.birthDate": "2019-05-08",
"mem.guardianSShip": 0,
"mem.realName": "测试",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "396ff1da-62a3-42d3-b8da-7b69a652ed4f"
},
{
"mem.birthDate": "2014-01-03",
"mem.guardianSShip": 13,
"mem.realName": "asddd7",
"mem.nickName": "",
"mem.memberGuid as memberGuid": "b1f300fb-06f5-461b-a8c6-e83f2fb9243c"
}
],
"offset": 0
}
}
分析
- 在网上查找好多资料发现除了使用DTO进行封装外只有一种方式,是在返回结果后再进行一次处理(代码如下)。这种方式虽然解决了上述的问题但是在返回的结果中包括的字段很多的情况写起来费时费力外还很容易出错。
public Map<String, Object> findAllMember(Pageable pageable) {
QMemberEntity qMember = new QMemberEntity("mem");
JPAQuery query = jpaQueryFactory.select(
Projections.map(
qMember.memberGuid.as("memberGuid"),
qMember.birthDate,
qMember.guardianSShip,
qMember.realName,
qMember.nickName))
.from(qMember)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
List<Tuple> rows = query.fetch();
Map<String, Object> result = new HashMap<>(5);
result.put("total", query.fetchCount());
result.put("data",rows.stream().map(row->{
Map map = new HashMap(row.size());
map.put("memberGuid",row.get(qMember.memberGuid));
map.put("birthDate",row.get(qMember.birthDate));
map.put("guardianSShip",row.get(qMember.guardianSShip));
map.put("realName",row.get(qMember.realName));
map.put("nickName",row.get(qMember.nickName));
return map;
}));
result.put("pageSize", pageable.getPageSize());
result.put("pageNumber", pageable.getPageNumber());
result.put("offset", pageable.getOffset());
return result;
}
- 没有找到通过配置方式解决上述问题的方法,可能不够细心吧。
- 没有办法只能看源码了,只过源码分析Projections.map是一个静态方法,在这个方法里new了一个QMap对象,源码如下。
/**
* Create a Map typed projection for the given expressions
*
* <p>Example</p>
* <pre>{@code
* Map<Expression<?>, ?> map = query.select(
* Projections.map(user.firstName, user.lastName));
* }</pre>
*
* @param exprs arguments for the projection
* @return factory expression
*/
public static QMap map(Expression<?>... exprs) {
return new QMap(exprs);
}
- 对源码进行单步调试(没有办法英文太烂看不懂官方文档),发现每次处理一条记录时都会调用QMap中的newInstance(Object… args)方法,通过源码可以看到在方法中有生成Map对象,方法中有两个比较关键的,一个是方法的参数args,这个对象是查询结果object[]对象,另一个是类属性args,这个对象是结果集字段的元数据对象列表。源码如下,只保留了QMap类的关键代码。
public class QMap extends FactoryExpressionBase<Map<Expression<?>,?>> {
private static final long serialVersionUID = -7545994090073480810L;
private final ImmutableList<Expression<?>> args;
/**
* Create a new QMap instance
*
* @param args
*/
@SuppressWarnings("unchecked")
protected QMap(Expression<?>... args) {
super((Class) Map.class);
this.args = ImmutableList.copyOf(args);
}
@Override
@Nullable
public Map<Expression<?>, ?> newInstance(Object... args) {
Map<Expression<?>, Object> map = Maps.newHashMap();
for (int i = 0; i < args.length; i++) {
map.put(this.args.get(i), args[i]);
}
return map;
}
}
- 通过调试源码发现调用newInstance方法是在调用JPAQuery.fetch()方法后被调用的,又进行跟踪发现序列化时生成的key就是字段元数据对象的toString值,所以判断重写newInstance方法可以定制返回结果集的数据。
- 分析QMap类的newInstance方法返回的是Map<Expression<?>, ?>,所以不能通过继承QMap重写newInstance,查看一下QMap父类FactoryExpressionBase类的源码(关键代码如下),发现newInstance方法的返回值是一个泛型,所以可以通过继承FactoryExpressionBase类重写newInstance方法实现定制返回结果集的数据。
/**
* Common superclass for {@link FactoryExpression} implementations
*
* @param <T>
*/
public abstract class FactoryExpressionBase<T> extends ExpressionBase<T> implements FactoryExpression<T> {
private static class FactoryExpressionWrapper<T> extends ExpressionBase<T> implements FactoryExpression<T> {
private final FactoryExpression<T> expr;
public FactoryExpressionWrapper(FactoryExpression<T> expr) {
super(expr.getType());
this.expr = expr;
}
@Override
public List<Expression<?>> getArgs() {
return expr.getArgs();
}
@Nullable
@Override
public T newInstance(Object... args) {
if (args != null) {
for (Object arg : args) {
if (arg != null) {
return expr.newInstance(args);
}
}
}
return null;
}
}
解决
通过上述的分析和代码调试的结果,参考QMap源码自定义了一个继承FactoryExpressionBase类的返回结果集类型的QueryResultMap类,经过测试满足我的需求,问题解决。源码如下。
package rm.lesi.mms;
import com.google.common.collect.ImmutableList;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.FactoryExpressionBase;
import com.querydsl.core.types.Visitor;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author
* @version 1.0.0
* @date 2020/5/26 3:32 下午
*/
public class QueryResultMap extends FactoryExpressionBase<Map<String,?>> {
private final ImmutableList<Expression<?>> args;
public QueryResultMap(Expression<?>... args) {
super((Class) Map.class);
this.args = ImmutableList.copyOf(args);
}
@Override
public List<Expression<?>> getArgs() {
return args;
}
@Override
@Nullable
public Map<String,?> newInstance(Object... args) {
Map<String, Object> map = new HashMap<>(args.length);
for (int i = 0; i < args.length; i++) {
String key = this.getArgs().get(i).toString();
if(key.contains(" as ")){
key = key.split(" as ")[1];
}else{
key = key.split("\\.")[1];
}
map.put(key, args[i]);
}
return map;
}
@Nullable
@Override
public <R, C> R accept(Visitor<R, C> v, @Nullable C context) {
return v.visit(this, context);
}
}
调用的时候在jpaQueryFactory.select里new QueryResultMap(),代码如下:
public Map<String, Object> findAllMember(Pageable pageable) {
QMemberEntity qMember = new QMemberEntity("mem");
JPAQuery query = jpaQueryFactory.select(
new QueryResultMap(
qMember.memberGuid.as("memberGuid"),
qMember.birthDate,
qMember.guardianSShip,
qMember.realName,
qMember.nickName))
.from(qMember)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
Map<String, Object> result = new HashMap<>(5);
result.put("total", query.fetchCount());
result.put("data",query.fetch());
result.put("pageSize", pageable.getPageSize());
result.put("pageNumber", pageable.getPageNumber());
result.put("offset", pageable.getOffset());
return result;
}
返回的执行结果如下:
"code": "200",
"msg": "ok",
"data": {
"pageSize": 10,
"total": 196,
"pageNumber": 0,
"data": [
{
"realName": "asddd",
"memberGuid": "c0091fe1-6faa-473d-9428-cf9f02fda899",
"guardianSShip": 12,
"birthDate": "2014-03-13",
"nickName": ""
},
{
"realName": "asddd1",
"memberGuid": "35406c3e-da06-426c-998b-c41e59c38941",
"guardianSShip": 12,
"birthDate": "2014-08-06",
"nickName": ""
},
{
"realName": "测试3",
"memberGuid": "94d04f7c-752a-4451-babf-a53c736de9da",
"guardianSShip": 12,
"birthDate": "2019-05-01",
"nickName": ""
},
{
"realName": "测试3",
"memberGuid": "926f53d1-aec8-4172-9c74-c4bbe7e0f92c",
"guardianSShip": 13,
"birthDate": "2009-05-08",
"nickName": ""
},
{
"realName": "asddd4",
"memberGuid": "31c5edc3-d1c6-4d33-a9ba-684fa77a7051",
"guardianSShip": 13,
"birthDate": "2015-12-23",
"nickName": ""
},
{
"realName": "asddd5",
"memberGuid": "4cf95517-5ff0-4c1a-98a4-019f2ebac1af",
"guardianSShip": 12,
"birthDate": "2014-10-11",
"nickName": ""
},
{
"realName": "asddd6",
"memberGuid": "f9ef258e-05f7-434b-9a35-928004bd129d",
"guardianSShip": 13,
"birthDate": "2011-12-31",
"nickName": ""
},
{
"realName": "测试2",
"memberGuid": "7c5ac178-9903-4c18-88d2-80310d77839d",
"guardianSShip": 0,
"birthDate": "2016-05-25",
"nickName": ""
},
{
"realName": "测试",
"memberGuid": "396ff1da-62a3-42d3-b8da-7b69a652ed4f",
"guardianSShip": 0,
"birthDate": "2019-05-08",
"nickName": ""
},
{
"realName": "asddd7",
"memberGuid": "b1f300fb-06f5-461b-a8c6-e83f2fb9243c",
"guardianSShip": 13,
"birthDate": "2014-01-03",
"nickName": ""
}
],
"offset": 0
}
}
结论
在spring data jpa +QueryDSL开发框架上进行开发使用起来还是很方便的,但是对应的文档还是比较少,在解决上述问题的过程中发现QueryDSL定制返回类型和结果集处理还是比较灵活。