使用 Spring 数据 JPA 进行复杂排序
Complex Sort with Spring Data JPA
我正在使用 Spring 数据规范和排序 API(我的存储库是 JpaSpecificationExecutor),我的数据模型看起来像这样(稍微精简)
@Entity
public class Message extends AbstractVersionedEntity {
@OneToMany
private Set<MessageOwner> messageOwners = new HashSet<>();
}
@Entity
public class MessageOwner extends AbstractVersionedEntity {
@ManyToOne
private Mailbox owner;
@Enumerated(EnumType.STRING)
private MessageOwnerType type;
}
@Entity
public class Mailbox extends AbstractVersionedEntity {
@Column(unique=true)
private String ldapId;
}
我需要按收件人对邮件进行排序 - 即按 MessageOwnerType.TO 类型的 messageOwners 邮箱的 ldapId。
message->messageOwners(type=TO)->owner->ldapId
此外,应该为每条消息对收件人进行排序 - 如果有多个类型为 MessageOwnerType.TO 的 messageOwner,可以这么说,应该选择按字母顺序排列的第一个作为消息的排序值。
在纯 SQL 中,我可能会使用某种子查询来执行此操作。使用 Spring Data Specifications & Sort 对象是否可以进行类似的操作?我宁愿不必重写 QueryDSL/raw 条件中的所有内容,除非我必须这样做。
好的,所以我想出了如何使用规范来做到这一点,但它并不漂亮。我无法使用 Sort 对象,而是使用 JPA CriteriaBuilder 将 orderBy 子句放在规范中。
这导致 SimpleJpaRepository 出现问题,它在所有可分页的 findAll 查询之前执行 select count()。我不得不禁用它(在 this Whosebug answer.
的帮助下
所以我的规范看起来像这样:(讨厌,我知道。如果有人有更好的建议,我很乐意听到他们)
@Override public Predicate toPredicate(Root<Message> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// Note - this would be simpler if JPA allowed subqueries in joins. As it doesn't,
// we have to put the subquery in a where clause, join both sides with the mailbox
// and correlate the results
// join the main query on the mailbox (for the ORDER BY clause)
Join<Message, MessageOwner> messageOwners = root.join("messageOwners", JoinType.LEFT);
Join<Message, Mailbox> mailbox = messageOwners.join("owner");
// create a subquery and correlate the messages with the main query
Subquery<String> subQuery = query.subquery(String.class);
Root<Message> sqMessage = subQuery.from(Message.class);
Root<Message> correlateMessage = subQuery.correlate(root);
// join the subquery on the mailbox
Join<Object, Object> sqMessageOwners = sqMessage.join("messageOwners", JoinType.LEFT);
Join<Message, Mailbox> sqMailbox = sqMessageOwners.join("owner");
// get the lowest ldapId alphabetically
Expression<String> minLdapId = cb.least(sqMailbox.<String>get("ldapId"));
// the actual subquery
// select the lowest ldapId for the current message (see the group-by clause)
// where the recipient type is TO and the message is the same as the main query
subQuery.select(minLdapId)
.where(
cb.and(
cb.equal(sqMessageOwners.get("type"), MessageOwnerType.TO),
cb.equal(sqMessage, correlateMessage)))
.groupBy(sqMessage.get("id"));
// the subquery gives us the lowest TO recipient for each mail
// sort on these values. Note: his must be done here rather than in the Sort property
// of the Pageable in order to maintain the connection between the mailbox in the order by clause
// and the one in the subquery
Path<String> recipientOrderClause = mailbox.get("ldapId");
Order recipientOrder = sortDirection == Sort.Direction.ASC ? cb.asc(recipientOrderClause) : cb.desc(recipientOrderClause);
// secondary sorting descending by sendTime
Order sendTimeOrder = cb.desc(root.get("sendTime"));
// adding order by queries here, rather than via the Sort object
// is something of a violation of standard Spring Data practice. (see above for the reason it is done)
// this precludes us from making some grouped calls such as select count(*)
query.orderBy(recipientOrder, sendTimeOrder);
// attach the subquery onto the query and return.
return cb.equal(mailbox.get("ldapId"), subQuery);
}
我正在使用 Spring 数据规范和排序 API(我的存储库是 JpaSpecificationExecutor),我的数据模型看起来像这样(稍微精简)
@Entity
public class Message extends AbstractVersionedEntity {
@OneToMany
private Set<MessageOwner> messageOwners = new HashSet<>();
}
@Entity
public class MessageOwner extends AbstractVersionedEntity {
@ManyToOne
private Mailbox owner;
@Enumerated(EnumType.STRING)
private MessageOwnerType type;
}
@Entity
public class Mailbox extends AbstractVersionedEntity {
@Column(unique=true)
private String ldapId;
}
我需要按收件人对邮件进行排序 - 即按 MessageOwnerType.TO 类型的 messageOwners 邮箱的 ldapId。
message->messageOwners(type=TO)->owner->ldapId
此外,应该为每条消息对收件人进行排序 - 如果有多个类型为 MessageOwnerType.TO 的 messageOwner,可以这么说,应该选择按字母顺序排列的第一个作为消息的排序值。
在纯 SQL 中,我可能会使用某种子查询来执行此操作。使用 Spring Data Specifications & Sort 对象是否可以进行类似的操作?我宁愿不必重写 QueryDSL/raw 条件中的所有内容,除非我必须这样做。
好的,所以我想出了如何使用规范来做到这一点,但它并不漂亮。我无法使用 Sort 对象,而是使用 JPA CriteriaBuilder 将 orderBy 子句放在规范中。
这导致 SimpleJpaRepository 出现问题,它在所有可分页的 findAll 查询之前执行 select count()。我不得不禁用它(在 this Whosebug answer.
的帮助下所以我的规范看起来像这样:(讨厌,我知道。如果有人有更好的建议,我很乐意听到他们)
@Override public Predicate toPredicate(Root<Message> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// Note - this would be simpler if JPA allowed subqueries in joins. As it doesn't,
// we have to put the subquery in a where clause, join both sides with the mailbox
// and correlate the results
// join the main query on the mailbox (for the ORDER BY clause)
Join<Message, MessageOwner> messageOwners = root.join("messageOwners", JoinType.LEFT);
Join<Message, Mailbox> mailbox = messageOwners.join("owner");
// create a subquery and correlate the messages with the main query
Subquery<String> subQuery = query.subquery(String.class);
Root<Message> sqMessage = subQuery.from(Message.class);
Root<Message> correlateMessage = subQuery.correlate(root);
// join the subquery on the mailbox
Join<Object, Object> sqMessageOwners = sqMessage.join("messageOwners", JoinType.LEFT);
Join<Message, Mailbox> sqMailbox = sqMessageOwners.join("owner");
// get the lowest ldapId alphabetically
Expression<String> minLdapId = cb.least(sqMailbox.<String>get("ldapId"));
// the actual subquery
// select the lowest ldapId for the current message (see the group-by clause)
// where the recipient type is TO and the message is the same as the main query
subQuery.select(minLdapId)
.where(
cb.and(
cb.equal(sqMessageOwners.get("type"), MessageOwnerType.TO),
cb.equal(sqMessage, correlateMessage)))
.groupBy(sqMessage.get("id"));
// the subquery gives us the lowest TO recipient for each mail
// sort on these values. Note: his must be done here rather than in the Sort property
// of the Pageable in order to maintain the connection between the mailbox in the order by clause
// and the one in the subquery
Path<String> recipientOrderClause = mailbox.get("ldapId");
Order recipientOrder = sortDirection == Sort.Direction.ASC ? cb.asc(recipientOrderClause) : cb.desc(recipientOrderClause);
// secondary sorting descending by sendTime
Order sendTimeOrder = cb.desc(root.get("sendTime"));
// adding order by queries here, rather than via the Sort object
// is something of a violation of standard Spring Data practice. (see above for the reason it is done)
// this precludes us from making some grouped calls such as select count(*)
query.orderBy(recipientOrder, sendTimeOrder);
// attach the subquery onto the query and return.
return cb.equal(mailbox.get("ldapId"), subQuery);
}