JPQL 按子列计算多个多对一和组计数
JPQL count multiple many-to-one and group counts by child column
我想构建一个 JPQL 查询以将该结构的数据映射到此 DTO:
@AllArgsConstructor
class UserDTO {
long userId;
long countOfContacts;
Map<String,Long> countOfActions; // by type
}
我不知道如何在 JPQL 中提取每个动作类型的计数,这就是我卡住的地方(看到我的名字了吗?:)):
public interface UserRepository extends CrudRepository<User, Long> {
@Query("SELECT new example.UserDTO( "
+ " u.id, "
+ " COUNT(contacts), "
--> + " ???group by actions.type to map<type,count>??? " <---
+ " ) "
+ " FROM User u "
+ " LEFT JOIN u.actions actions "
+ " LEFT JOIN u.contacts contacts "
+ " GROUP BY u.id")
List<UserDTO> getAll();
}
我使用 postgres,如果这在 JPQL 中不可能,也可以使用本机查询。
其实我可以通过原生查询和映射Java中的动作数据来解决,但感觉很糟糕:
SELECT
u.id,
COALESCE(MIN(countOfContacts.count), 0) as countOfContacts,
ARRAY_TO_STRING(ARRAY_REMOVE(ARRAY_AGG(actions.type || ':' || actions.count), null),',') AS countOfActions
FROM user u
LEFT JOIN (
SELECT
user_id, COUNT(*) as count
FROM contact
GROUP BY user_id
) countOfContacts
ON countOfContacts.user_id = u.id
LEFT JOIN (
SELECT
user_id, type, COUNT(*)
FROM action
GROUP BY user_id, type
) actions
ON actions.user_id = u.id
GROUP BY u.id
;
得出这样的结果数据:
id | countOfContacts | countOfActions
--------+-----------------+-------------------------
11728 | 0 | {RESTART:2}
9550 | 0 | {}
9520 | 0 | {CLEAR:1}
12513 | 0 | {RESTART:2}
10238 | 3 | {CLEAR:2,RESTART:5}
16531 | 0 | {CLEAR:1,RESTART:7}
9542 | 0 | {}
...
由于在本机查询中我们无法映射到 POJO,我 return List<String[]>
并自行将所有列转换为 UserDTO
的构造函数:
@Query(/*...*/)
/** use getAllAsDTO for a typed result set */
List<String[]> getAll();
default List<UserDTO> getAllAsDTO() {
List<String[]> result = getAll();
List<UserDTO> transformed = new ArrayList<>(result.size());
for (String[] row : result) {
long userId = Long.parseLong(row[0]);
long countOfContacts = Long.parseLong(row[1]);
String countOfActions = row[2];
transformed.add(
new UserDTO(userId, countOfContacts, countOfActions)
);
}
return transformed;
}
然后我在 DTO 的构造函数中将 countOfActions
映射到 Java Map<String, Long>
:
class UserDTO {
long userId;
long countOfContacts;
Map<String,Long> countOfActions; // by type
/**
* @param user
* @param countOfContacts
* @param countOfActions {A:1,B:4,C:2,..} will not include keys for 0
*/
public UserDTO(long userId, long countOfContacts, String countOfActionsStr) {
this.userId = userId;
this.countOfContacts = countOfContacts;
this.countOfActions = new HashMap<>();
// remove curly braces
String s = countOfActionsStr.replaceAll("^\{|\}$", "");
if (s.length() > 0) { // exclude empty "arrays"
for (String item : s.split(",")) {
String[] tmp = item.split(":");
String action = tmp[0];
long count = Long.parseLong(tmp[1]);
countOfActions.put(action, count);
}
}
}
}
DB层能解决吗?
不幸的是JPQL doesn't have an aggregation function like string_agg or group_concat。所以你应该自己转换查询结果。首先,您应该像这样创建一个 "plain" 查询,例如:
@Query("select new example.UserPlainDto( " +
" a.user.id, " +
" count(distinct c.id), " +
" a.type, " +
" count(distinct a.id) " +
") " +
"from " +
" Action a " +
" join Contact c on c.user.id = a.user.id " +
"group by " +
" a.user.id, a.type")
List<UserPlainDto> getUserPlainDtos();
(它是 HQL - JPQL 的 Hibernate 扩展)
此查询的结果将是普通的 table,例如:
|--------|---------------|-------------|-------------|
|user_id |countact_count |action_type |action_count |
|--------|---------------|-------------|-------------|
|1 |3 |ONE |1 |
|1 |3 |TWO |2 |
|1 |3 |THREE |3 |
|2 |2 |ONE |1 |
|2 |2 |TWO |2 |
|3 |1 |ONE |1 |
|--------|---------------|-------------|-------------|
然后你应该将该结果分组到 UserDto
的集合中,像这样:
default Collection<UserDto> getReport() {
Map<Long, UserDto> result = new HashMap<>();
getUserPlainDtos().forEach(dto -> {
long userId = dto.getUserId();
long actionCount = dto.getActionCount();
UserDto userDto = result.getOrDefault(userId, new UserDto());
userDto.setUserId(userId);
userDto.setContactCount(dto.getContactCount());
userDto.getActions().compute(dto.getActionType(), (type, count) -> count != null ? count + actionCount : actionCount);
result.put(userId, userDto);
});
return result.values();
}
那么瞧,你会在Collection<UserDto>
中得到这样的结果:
[
{
"userId": 1,
"contactCount": 3,
"actions": {
"ONE": 1,
"TWO": 2,
"THREE": 3
}
},
{
"userId": 2,
"contactCount": 2,
"actions": {
"ONE": 1,
"TWO": 2
}
},
{
"userId": 3,
"contactCount": 1,
"actions": {
"ONE": 1
}
}
]
上面使用了DTO:
@Value
class UserPlainDto {
long userId;
long contactCount;
ActionType actionType;
long actionCount;
}
@Data
class UserDto {
long userId;
long contactCount;
Map<ActionType, Long> actions = new HashMap<>();
}
我想构建一个 JPQL 查询以将该结构的数据映射到此 DTO:
@AllArgsConstructor
class UserDTO {
long userId;
long countOfContacts;
Map<String,Long> countOfActions; // by type
}
我不知道如何在 JPQL 中提取每个动作类型的计数,这就是我卡住的地方(看到我的名字了吗?:)):
public interface UserRepository extends CrudRepository<User, Long> {
@Query("SELECT new example.UserDTO( "
+ " u.id, "
+ " COUNT(contacts), "
--> + " ???group by actions.type to map<type,count>??? " <---
+ " ) "
+ " FROM User u "
+ " LEFT JOIN u.actions actions "
+ " LEFT JOIN u.contacts contacts "
+ " GROUP BY u.id")
List<UserDTO> getAll();
}
我使用 postgres,如果这在 JPQL 中不可能,也可以使用本机查询。
其实我可以通过原生查询和映射Java中的动作数据来解决,但感觉很糟糕:
SELECT
u.id,
COALESCE(MIN(countOfContacts.count), 0) as countOfContacts,
ARRAY_TO_STRING(ARRAY_REMOVE(ARRAY_AGG(actions.type || ':' || actions.count), null),',') AS countOfActions
FROM user u
LEFT JOIN (
SELECT
user_id, COUNT(*) as count
FROM contact
GROUP BY user_id
) countOfContacts
ON countOfContacts.user_id = u.id
LEFT JOIN (
SELECT
user_id, type, COUNT(*)
FROM action
GROUP BY user_id, type
) actions
ON actions.user_id = u.id
GROUP BY u.id
;
得出这样的结果数据:
id | countOfContacts | countOfActions
--------+-----------------+-------------------------
11728 | 0 | {RESTART:2}
9550 | 0 | {}
9520 | 0 | {CLEAR:1}
12513 | 0 | {RESTART:2}
10238 | 3 | {CLEAR:2,RESTART:5}
16531 | 0 | {CLEAR:1,RESTART:7}
9542 | 0 | {}
...
由于在本机查询中我们无法映射到 POJO,我 return List<String[]>
并自行将所有列转换为 UserDTO
的构造函数:
@Query(/*...*/)
/** use getAllAsDTO for a typed result set */
List<String[]> getAll();
default List<UserDTO> getAllAsDTO() {
List<String[]> result = getAll();
List<UserDTO> transformed = new ArrayList<>(result.size());
for (String[] row : result) {
long userId = Long.parseLong(row[0]);
long countOfContacts = Long.parseLong(row[1]);
String countOfActions = row[2];
transformed.add(
new UserDTO(userId, countOfContacts, countOfActions)
);
}
return transformed;
}
然后我在 DTO 的构造函数中将 countOfActions
映射到 Java Map<String, Long>
:
class UserDTO {
long userId;
long countOfContacts;
Map<String,Long> countOfActions; // by type
/**
* @param user
* @param countOfContacts
* @param countOfActions {A:1,B:4,C:2,..} will not include keys for 0
*/
public UserDTO(long userId, long countOfContacts, String countOfActionsStr) {
this.userId = userId;
this.countOfContacts = countOfContacts;
this.countOfActions = new HashMap<>();
// remove curly braces
String s = countOfActionsStr.replaceAll("^\{|\}$", "");
if (s.length() > 0) { // exclude empty "arrays"
for (String item : s.split(",")) {
String[] tmp = item.split(":");
String action = tmp[0];
long count = Long.parseLong(tmp[1]);
countOfActions.put(action, count);
}
}
}
}
DB层能解决吗?
不幸的是JPQL doesn't have an aggregation function like string_agg or group_concat。所以你应该自己转换查询结果。首先,您应该像这样创建一个 "plain" 查询,例如:
@Query("select new example.UserPlainDto( " +
" a.user.id, " +
" count(distinct c.id), " +
" a.type, " +
" count(distinct a.id) " +
") " +
"from " +
" Action a " +
" join Contact c on c.user.id = a.user.id " +
"group by " +
" a.user.id, a.type")
List<UserPlainDto> getUserPlainDtos();
(它是 HQL - JPQL 的 Hibernate 扩展)
此查询的结果将是普通的 table,例如:
|--------|---------------|-------------|-------------|
|user_id |countact_count |action_type |action_count |
|--------|---------------|-------------|-------------|
|1 |3 |ONE |1 |
|1 |3 |TWO |2 |
|1 |3 |THREE |3 |
|2 |2 |ONE |1 |
|2 |2 |TWO |2 |
|3 |1 |ONE |1 |
|--------|---------------|-------------|-------------|
然后你应该将该结果分组到 UserDto
的集合中,像这样:
default Collection<UserDto> getReport() {
Map<Long, UserDto> result = new HashMap<>();
getUserPlainDtos().forEach(dto -> {
long userId = dto.getUserId();
long actionCount = dto.getActionCount();
UserDto userDto = result.getOrDefault(userId, new UserDto());
userDto.setUserId(userId);
userDto.setContactCount(dto.getContactCount());
userDto.getActions().compute(dto.getActionType(), (type, count) -> count != null ? count + actionCount : actionCount);
result.put(userId, userDto);
});
return result.values();
}
那么瞧,你会在Collection<UserDto>
中得到这样的结果:
[
{
"userId": 1,
"contactCount": 3,
"actions": {
"ONE": 1,
"TWO": 2,
"THREE": 3
}
},
{
"userId": 2,
"contactCount": 2,
"actions": {
"ONE": 1,
"TWO": 2
}
},
{
"userId": 3,
"contactCount": 1,
"actions": {
"ONE": 1
}
}
]
上面使用了DTO:
@Value
class UserPlainDto {
long userId;
long contactCount;
ActionType actionType;
long actionCount;
}
@Data
class UserDto {
long userId;
long contactCount;
Map<ActionType, Long> actions = new HashMap<>();
}