【问题标题】:JPA: How do I sort on a Set field in my entity?JPA:如何对实体中的 Set 字段进行排序?
【发布时间】:2015-01-09 20:06:58
【问题描述】:

我正在使用 Spring 3.2.11.RELEASE、Hibernate 4.3.6.Final 和 JPA 2.1。我有以下具有以下字段的实体......

@Entity
@Table(name = "user")
public class User implements Serializable, Comparable<User>
{
    …
    @ManyToMany
    @JoinTable(name = "user_organization", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ORGANIZATION_ID") })
    @LazyCollection(LazyCollectionOption.FALSE)
    @SortNatural
    private SortedSet<Organization> organizations;

在上面,组织的排序是通过组织的名称字段完成的。当我运行 JPA 查询来检索用户对象时,我想根据与其关联的组织的有序列表进行排序。这个我试过了……

final CriteriaBuilder builder = m_entityManager.getCriteriaBuilder();
final CriteriaQuery<User> criteria = builder.createQuery(User.class);
…
final SetJoin<User, Organization> orgsJoin = orderByRoot.join(User_.organizations, JoinType.LEFT);
orderByExpr = orgsJoin.get(Organization_.name);
criteria.orderBy(builder.asc(orderByExpr);

但它不适用于用户与多个组织相关联的情况 - 有时,在其列表中具有按字母顺序排列较低组织的用户会在正确用户之前返回。如果不写一大堆原生 SQL,我怎么能用 JPA/CriteriaBuilder 解决这个问题?

编辑:这是我想要的一个例子。我正在寻找不同的用户。

  • 用户 A 有组织,“AAA 大楼”和“ZZZ 大楼”
  • 用户 B 的组织只有“MMM 大楼”
  • 用户 C 拥有组织“CCC 大楼”和“VVV 大楼”
  • 用户 D 拥有组织“AAA Building”和“MMM Buidling”

我想要用户 D、用户 A、用户 C 和用户 B,因为用户 A 的组织的串联按字母顺序低于用户 C 的组织的串联,而用户 C 的组织按字母顺序低于用户 B 的组织。

【问题讨论】:

  • 那个 JPQL 生成的 SQL 是什么?
  • 你只选择不同的用户吗?
  • “不用写一大堆原生 SQL” - 问题就在这里。您正在尝试使用一堆注释来解决 SQL 问题...
  • 这是关于什么数据库的?

标签: java hibernate sorting jpa orm


【解决方案1】:

您不能为实体编写这样的查询。如果您使用的是本机 SQL,您可以按 user.id 对组织名称进行 GROUP BY,但这不是您想要的。

您可以简单地获取加入用户和组织:

final CriteriaBuilder builder = m_entityManager.getCriteriaBuilder();
final CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
Fetch<User, Organization> organizations = root.fetch("organizations");
criteria.select(c);
TypedQuery<User> query = em.createQuery(criteria);
List<User> users = query.getResultList();

并在内存中对它们进行排序:

Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User user1, User user2) {
        SortedSet<Organization> organizations1 = user1.getOrganizations();
        SortedSet<Organization> organizations2 = user2.getOrganizations();

        if(organizations1.isEmpty()) {
            if(organizations2.isEmpty()) {
                return 0;
            } else {
                return -1;
            }
        } else {
            if(organizations2.isEmpty()) {
                return 1;
            } else {
                Organization o1 = organizations1.first();
                Organization o2 = organizations2.first();
                return o1.compareTo(o2);
            }
        }
    }
});

更新

如果您不想获取内存中的所有内容进行排序,那么我认为仅使用 JPQL 或 Criteria API 是不可能的。

假设组织名称是 VARCHAR(50),我们需要先使用右填充,然后我们需要先运行 group_concat 原生查询:

PostgrSql

SELECT o.user_id, 
       string_agg(RPAD(o.name, 50) ORDER BY o.name) as ordr
FROM Organization o
GROUP BY o.user_id
order by ordr   

MySQL

SELECT o.user_id, 
       group_concat(RPAD(o.name, 50) ORDER BY o.name) as ordr
FROM Organization o
GROUP BY o.user_id
order by ordr

对 user_id 进行排序后,您可以简单地将 user_ids 提取到 List&lt;Long&gt;,然后运行第二个 JPQL 查询来获取用户:

List<Long> userIds = ...;

TypedQuery<User> q = em.createQuery(
    "select u " +
    "from User u " +
    "where u.id in :userIds "
, User.class);

q.setParameter("userIds", userIds);
List<User> users = q.getResultList();

【讨论】:

  • 嗨,您可能已经看到我关于仅对第一个组织进行排序的编辑。此外,检索用户组并在之后进行排序通常可以工作,但我想在查询中进行,因为应用程序的一部分只要求特定组的结果(例如结果 50-99)所以除非首先对查询进行排序,否则我不知道它们是什么。我不想带回数千个结果,在内存中对它们进行排序,然后从中选择特定的结果组,除非这是唯一的方法。
  • 您知道 JPA 2.1 支持使用数据库原生函数,例如您建议的 GROUP_CONCAT。我想知道这是否对我的 CriteriaBuilder 愿望有价值?
  • 我认为 JPA 支持基本的 SQL 兼容字符串函数(LOWER、CONCAT),但肯定不支持 GROUP_CONCAT。虽然我给你举了一个 MySQL 和 Postgres 的例子,但我记得 SQL Server 并不直接支持它,因此无法标准化。
  • 对,不是说有直接的 GROUP_CONCAT 函数,而是可以使用类似的东西 -- cb.function("GROUP_CONCAT", String.class, orgsJoin.get(Organization_.name))。到目前为止,我还没有解决这个问题,所以也许我提出的问题毕竟没有解决方案。
  • 您的查询中还有一个错误。您正在连接未排序的名称。使用group_concat(RPAD(o.name, 50, ' ') order by o.name)
【解决方案2】:

您必须在获取用户列表后对其进行排序。在执行此操作之前,您必须对 User 类添加一些更改:

public class User implements Serializable, Comparable<User>, Comparator<User>
{
    ...

    public int compareTo(User u) {
        return compare(this, u);
    }

    public int compare(User u1, User u2) {
        Iterator<Organization> it1 = u1.organizations.iterator();
        Iterator<Organization> it2 = u2.organizations.iterator();
        while (it1.hasNext() && it2.hasNext()) {
            Organization o1 = it1.next();
            Organization o2 = it2.next();
            if(o1.compareTo(o2) != 0) {
                return o1.compareTo(o2);
            }
        }
        if(it1.hasNext())
            return 1;
        if(it2.hasNext())
            return -1;  
        return 0;
    }
}

Organization类:

public class Organization implements Comparable<Organization>
{
    ...

    public int compareTo(Organization o) {
        return (this.name).compareTo(o.name);
    }
}

然后您可以调用以下代码获得排序的用户列表:

final CriteriaBuilder builder = m_entityManager.getCriteriaBuilder();
final CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
Join<User, Organization> organizations = root.fetch("organizations");
criteria.select(root);
TypedQuery<User> query = em.createQuery(criteria);
List<User> users = query.getResultList();
Collections.sort(users, new User());

以上代码将按第一个组织的名称对用户进行排序。但如果这些名称相同,则将比较第二个组织的名称,依此类推。

编辑:

  1. 我不得不承认,我的上述解决方案对于大量 用户。
  2. 有两个答案建议使用string_agg(o.name, ',') 或类似函数进行SQL 查询。这些查询并不总是正确排序。让我们看下面的例子:

    • User1:只有组织“AAA”
    • User2:只有组织“AAAA”
    • User3:拥有组织“AAA”和“BBB”
    • User4:拥有组织“AAAA”和“BBB”

使用string_agg(o.name, ',')排序后,我们将收到以下订单:

  • 用户 1“AAA”
  • 用户2“AAAA”
  • 用户4“AAAA,BBB”
  • 用户3“AAA,BBB”

这是不正确的。

那么让我为您提供另一个解决方案:

organizationNames 字段添加到User 类。在此字段中,您将存储字符串,该字符串是组织的排序名称的串联。
在连接之前,每个名称都应使用空格扩展至最大大小。
您的四个模范用户将如下所示:

  • 用户AorganizationNames = "AAA Building ZZZ Building "
  • 用户BorganizationNames = "MMM Building "
  • 用户CorganizationNames = "CCC Building VVV Buidling "
  • 用户DorganizationNames = "AAA Building MMM Buidling "

所以这会增加一些冗余。此字段的值应在更改用户的组织列表后更新。
然后您可以简单地按organizationNames 字段对用户进行排序。您可以在该列上添加索引,这样的排序应该很快。它不需要任何连接或子查询。

【讨论】:

  • 您好,我不想更改整个应用程序中用户的默认排序。我只是想在我的 JPA 中添加一个子句,允许基于组织列表进行排序。我提供的代码 sn-p 是几个排序子句之一。在应用程序中,最终用户能够对用户名、用户电子邮件和用户组织进行排序,这正是我想要弄清楚的。
  • @rtruszk 你确定你没有采纳我的想法吗? :)
  • @AndreiI 您的解决方案仅限于第一个组织。而且您无法将其扩展到所有列表。您甚至建议添加方向错误的 secondOrganization 字段。所以我认为你最初的方向是好的,但后来你卡住了。
  • @rtruszk 你避免回答我的问题 :) 真诚地我改变了我的回答添加了连接的想法,没有阅读你的。反正干杯:)
【解决方案3】:

如果您想选择不同的用户,那么您无法使用您拥有的代码来执行此操作,因为不清楚按哪个组织进行排序:按第一个、第二个、...还是按最后一个。

但是有一个可行的解决方案(假设您希望始终按第一个组织进行排序):

  1. 添加新属性private String organisationsAsString; 每次更改User 时,都会将此属性设置为集合中Organisation 的串联列表。
  2. 然后您只需调整您的 CriteriaQuery 以改为按新属性排序。

当然,您可以在获取Users 之后对其进行排序,但这可能有一个很大的缺点:如果您(将)有几万个,您将需要对它们进行分页。然后,按其他字段排序会复杂得多。同时加载许多用户,以及他们的关系可能是一个很大的性能瓶颈。如果您只有几百个用户,这个解决方案肯定会奏效。

【讨论】:

  • 我想要不同的用户。我编辑了我的问题以包含一个示例,说明我希望如何在假设的场景中返回结果。
  • 那么您想按集合中的第一个组织对它们进行排序吗?检查我更新的答案。
  • 不只是第一个组织。我再次编辑了我的示例。如果有两个用户具有相同的第一个组织,那么我希望决胜局成为第二个组织。同样,如果两个用户的第一个和第二个相同,那么我希望决胜局是第三个,依此类推。
  • 戴夫,你可能明白“等等”的意思是无限的。我的意思是,即使您编写本机查询,您也必须在某个地方停下来(例如,在第二个Organisation,如果与第一个组织有联系)。如果我说服了您,那么只需为 secondOrganisation 添加另一个属性。有关其他答案的评论,请参阅我的更新答案。
  • 好的,谢谢。理想情况下,我想要一个涵盖所有情况的解决方案,而不仅仅是用户拥有 2 个组织的解决方案,或者其他任何情况。但如果没有办法按照我的要求去做,那就这样吧。
猜你喜欢
  • 1970-01-01
  • 2018-07-21
  • 2015-04-06
  • 1970-01-01
  • 1970-01-01
  • 2021-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多