【问题标题】:JPQL count multiple many-to-one and group counts by child columnJPQL 按子列计数多个多对一和组计数
【发布时间】:2020-04-26 12:08:18
【问题描述】:

我想构建一个 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中的actions数据映射来解决,但是感觉很糟糕:

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,所以我返回 List&lt;String[]&gt; 并自行将所有列转换为 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&lt;String, Long&gt;

    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 层解决了吗?

【问题讨论】:

    标签: java sql postgresql jpa jpql


    【解决方案1】:

    很遗憾,JPQL 没有像 string_agggroup_concat 这样的聚合函数。所以你应该自己转换查询结果。首先,您应该像这样创建一个“普通”查询,例如:

    @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 扩展)

    这个查询的结果将是一个普通的表,例如:

    |--------|---------------|-------------|-------------|
    |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&lt;UserDto&gt;得到这样的结果:

    [
        {
            "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<>();
    }
    

    My demo project.

    【讨论】:

    • 虽然问题是关于利用 DB 层,但我从这个答案中了解到我们可能无法做到这一点。就易于掌握和打字安全而言,所提供的解决方案至少非常干净。谢谢
    • 不幸的是,这在分页时不起作用,因为页面大小将在后处理之前应用于具有重复顶级对象的行。
    • 对于分页另一个查询应该首先选择页面数据,然后可以将给定的解决方案限制为相关用户。如果分页取决于计数,则物化视图会有所帮助。
    猜你喜欢
    • 1970-01-01
    • 2017-01-08
    • 2011-03-13
    • 1970-01-01
    • 1970-01-01
    • 2017-05-09
    • 1970-01-01
    • 2020-02-10
    • 1970-01-01
    相关资源
    最近更新 更多