У меня есть сущность Player, которая имеет @OneToMany связь с PlayerTransfer. Я хочу отсортировать игроков по бюджету последнего трансфера.
Вот резюме сущностей:
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Player {
// другие поля...
@OneToMany(mappedBy = "playerTransfer")
@Builder.Default
private List<PlayerTransfer> playerTransferList = new ArrayList<>();
}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class PlayerTransfer {
// другие поля
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "player_id")
private Player player;
private OffsetDateTime transferTime;
private Integer transferRate;
private Integer budget;
}
Попытка-1
Я написал метод спецификации, как показано ниже, но он вызывает ошибку SQL [ERROR: more than one row returned by a subquery used as an expression\]
, если у игрока более одного трансфера.
public static Specification<Player> orderByBudget(boolean desc) {
return (root, query, criteriaBuilder) -> {
Subquery<PlayerTransfer> sQuery = query.subquery(PlayerTransfer.class);
Root<PlayerTransfer> subRoot = sQuery.from(PlayerTransfer.class);
Subquery<PlayerTransfer> subquery= sQuery.select(subRoot).where(criteriaBuilder.equal(subRoot.get("player"), root));
Join<Player, PlayerTransfer> playerTransferJoin = root.join("playerTransferList", JoinType.LEFT);
Order order;
if (desc) {
order = criteriaBuilder.desc(criteriaBuilder.selectCase()
.when(criteriaBuilder.isNotNull(playerTransferJoin.get("budget")), criteriaBuilder.selectCase()
.when(criteriaBuilder.equal(playerTransferJoin, subquery), playerTransferJoin.get("budget"))));
} else {
order = criteriaBuilder.asc(criteriaBuilder.selectCase()
.when(criteriaBuilder.isNotNull(playerTransferJoin.get("budget")), criteriaBuilder.selectCase()
.when(criteriaBuilder.equal(playerTransferJoin, subquery), playerTransferJoin.get("budget"))));
}
List<Order> orders = new ArrayList<>(query.getOrderList());
orders.add(order);
query.orderBy(orders);
return query.getRestriction();
};
}
Попытка-2
public static Specification<Player> orderByBudget(boolean desc) {
return (root, query, criteriaBuilder) -> {
Subquery<Long> subquery = query.subquery(Long.class);
Root<PlayerTransfer> statusRoot = subquery.from(PlayerTransfer.class);
subquery.select(criteriaBuilder.greatest(statusRoot.get("budget").as(Long.class))).where();
subquery.where(criteriaBuilder.equal(statusRoot.get("player"), root));
Order order;
if (desc) {
order = criteriaBuilder.desc(subquery);
} else {
order = criteriaBuilder.asc(subquery);
}
List<Order> orders = new ArrayList<>(query.getOrderList());
orders.add(order);
query.orderBy(orders);
return query.getRestriction();
};
}
Моя другая попытка представлена выше, этот метод не вызывает никаких исключений, но он не работает так, как я хочу. Поскольку подзапрос выбирает максимальный бюджет и сортирует по нему, я хочу получить бюджет последнего трансфера. Можете ли вы помочь мне с этой проблемой.
В резюме, я хочу отсортировать игроков по бюджету последнего трансфера.
Ответ
Чтобы решить вашу задачу и отсортировать игроков по бюджету их последних трансферов, необходимо пересмотреть реализацию спецификации. Можем использовать подзапрос для получения последнего трансфера каждого игрока и затем использовать соответствующий бюджет для упорядочивания.
Ниже приведён пример того, как можно реализовать эту логику:
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
public static Specification<Player> orderByLatestTransferBudget(boolean desc) {
return (root, query, criteriaBuilder) -> {
// Указываем, что мы хотим получить только последние трансферы каждого игрока
Subquery<PlayerTransfer> subquery = query.subquery(PlayerTransfer.class);
Root<PlayerTransfer> subRoot = subquery.from(PlayerTransfer.class);
// Подзапрос будет возвращать последний трансфер для каждого игрока
subquery.select(subRoot)
.where(criteriaBuilder.equal(subRoot.get("player"), root))
.orderBy(criteriaBuilder.desc(subRoot.get("transferTime")));
// Убираем дублирования, чтобы вернуть только последний трансфер
subquery.distinct(true);
// Создаём условие на бюджеты последних трансферов
query.select(root)
.where(criteriaBuilder.equal(subRoot, subquery))
.distinct(true);
// Определяем порядок сортировки по бюджету последнего трансфера
Expression<Integer> latestTransferBudget = criteriaBuilder.coalesce(
criteriaBuilder.selectCase()
.when(criteriaBuilder.exists(subquery), subRoot.get("budget")),
0); // Значение по умолчанию, если трансферов нет
// Добавляем сортировку
if (desc) {
query.orderBy(criteriaBuilder.desc(latestTransferBudget));
} else {
query.orderBy(criteriaBuilder.asc(latestTransferBudget));
}
return query.getRestriction();
};
}
Объяснение кода:
-
Подзапрос (Subquery):
- Мы создаём подзапрос, чтобы получить список последних трансферов для каждого игрока на основе даты
transferTime
. - Условие
when(criteriaBuilder.exists(subquery), subRoot.get("budget"))
позволяет нам получить бюджет каждого последнего трансфера.
- Мы создаём подзапрос, чтобы получить список последних трансферов для каждого игрока на основе даты
-
Сортировка:
- Используем
criteriaBuilder.coalesce(...)
для обработки случаев, где у игрока может не быть трансферов (значение 0, если трансферов нет). - В зависимости от параметра
desc
, устанавливается сортировка по бюджету последних трансферов.
- Используем
- Возвращаемое значение:
- Мы возвращаем
query.getRestriction()
для ограничения выборки на основе принятого условия.
- Мы возвращаем
Эта спецификация теперь должна успешно сортировать игроков по бюджету их последних трансферов. Протестируйте это решение, чтобы убедиться, что оно работает в соответствии с вашими ожиданиями.