JPQL подсчитывает несколько многозначных и групповых подсчетов по дочернему столбцу - PullRequest
3 голосов
/ 09 января 2020

enter image description here

Я хочу создать запрос 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, я возвращаю 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;
}

Затем я сопоставляю countOfActions с Java Map<String, Long> в конструкторе DTO:

    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);
              }
            }
        }
    }

Могу ли я решить его уже на слое БД?

1 Ответ

3 голосов
/ 21 января 2020

К сожалению JPQL не имеет функции агрегирования, такой как string_agg или group_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 - расширение Hibernate в JPQL)

Результат этого запрос будет простой таблицей, например:

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

Мой демонстрационный проект .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...