JPA:如何对实体中的Set字段进行排序?

我使用的是Spring 3.2.11.RELEASE,Hibernate 4.3.6.Final和JPA 2.1。 我有以下实体与以下字段…

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

上面,组织的排序由组织的名称字段完成。 当我运行JPA查询来检索User对象时,我想基于与它们关联的组织的有序列表进行排序。 我试过这个……

 final CriteriaBuilder builder = m_entityManager.getCriteriaBuilder(); final CriteriaQuery criteria = builder.createQuery(User.class); … final SetJoin 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 Building”和“VVV Buidling”组织
  • 用户D拥有“AAA Building”和“MMM Buidling”组织

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

您无法为实体编写此类查询。 如果您使用的是本机SQL,那么user.id可以组织GROUP BY组织名称,但这不是您想要的。

您只需获取加入用户和组织:

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

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

 Collections.sort(users, new Comparator() { @Override public int compare(User user1, User user2) { SortedSet organizations1 = user1.getOrganizations(); SortedSet 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 ,然后运行第二个JPQL查询即可获取用户:

 List userIds = ...; TypedQuery q = em.createQuery( "select u " + "from User u " + "where u.id in :userIds " , User.class); q.setParameter("userIds", userIds); List users = q.getResultList(); 

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

 public class User implements Serializable, Comparable, Comparator { ... public int compareTo(User u) { return compare(this, u); } public int compare(User u1, User u2) { Iterator it1 = u1.organizations.iterator(); Iterator 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 { ... public int compareTo(Organization o) { return (this.name).compareTo(o.name); } } 

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

 final CriteriaBuilder builder = m_entityManager.getCriteriaBuilder(); final CriteriaQuery criteria = builder.createQuery(User.class); Root root = criteria.from(User.class); Join organizations = root.fetch("organizations"); criteria.select(root); TypedQuery query = em.createQuery(criteria); List 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, ',')对其进行排序后string_agg(o.name, ',')我们将收到以下订单:

  • 用户1“AAA”
  • 用户2“AAAA”
  • User4“AAAA,BBB”
  • User3“AAA,BBB”

哪个不对。

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

organizationNames字段添加到User类。 在此字段中,您将存储字符串,该字符串是组织的已排序名称的串联。
在连接之前,每个名称都应该使用空格扩展到最大大小。
您的四个示例用户看起来像这样:

  • 用户A organizationNames = "AAA Building ZZZ Building "
  • 用户B organizationNames = "MMM Building "
  • 用户C organizationNames = "CCC Building VVV Buidling "
  • 用户D organizationNames = "AAA Building MMM Buidling "

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

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

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

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

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