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());
以上代码将按照第一组织的名称对用户进行排序。 但如果这些名称相同,那么第二组织的名称将被比较,依此类推。
编辑:
- 我必须承认,我的上述解决方案对大量用户不利。
-
使用
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
字段对用户进行排序。 您可以在该列上添加索引,这样的排序应该很快。 它不需要任何连接或子查询。
如果您想选择不同的用户,那么您无法使用您拥有的代码,因为不清楚要对哪个组织进行排序:按第一个,第二个,……还是最后一个。
但是有一个解决方案可行(假设您希望始终由第一个组织排序):
- 添加新属性
private String organisationsAsString;
每次更改User
,都会将此属性设置为集合中的Organisation
连接列表。 - 然后,您只需调整CriteriaQuery以按新属性进行排序。
当然,你可以在获取后对Users
进行排序,但这可能有一个很大的缺点:如果你(将)有几十万,你会想要对它们进行分页。 然后,按其他字段排序将会复杂得多。 加载许多用户,与他们的关系可能是一个巨大的性能瓶颈。 如果您只有几百个用户,这个解决方案肯定会有效。
- 如何在@HandleBeforeSave事件中获取旧实体值以确定属性是否已更改?
- 错误:无法创建PoolableConnectionFactory(isValid()返回false)
- 查找与所选点的特定距离内的所有地址的最佳方法是什么
- org.postgresql.util.PGobject无法强制转换为org.postgis.PGgeometry
- AuditException:由于非活动事务而无法创建修订
- JPA还是Hibernate for Java Persistence?
- 保存对数据库vaadin的更改
- oracle 11g和hibernate spring和jsf的集成
- hibernate没有创建表但没有错误消息