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

时间:2015-01-09 20:06:58

标签: java hibernate sorting jpa orm

我正在使用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查询来检索User对象时,我想根据与之关联的组织的有序列表进行排序。我试过这个......

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 Building”和“ZZZ Building”
  • 用户B只有“MMM大楼”组织
  • 用户C拥有“CCC Building”和“VVV Buidling”组织
  • 用户D拥有“AAA Building”和“MMM Buidling”组织

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

3 个答案:

答案 0 :(得分:3)

您无法为实体编写此类查询。如果您使用的是原生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<Long>,然后运行第二个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();

答案 1 :(得分:1)

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

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

  1. 添加新属性private String organisationsAsString;每次更改User时,都会将此属性设置为集合中Organisation的连续列表。
  2. 然后,您只需调整CriteriaQuery,然后按新属性进行排序。
  3. 当然你可以在获取它们之后对Users进行排序,但这可能有一个很大的缺点:如果你(将)有几十万,你会想要对它们进行分页。然后,按其他字段排序将会复杂得多。加载许多用户,与他们的关系可能是一个巨大的性能瓶颈。如果您只有几百个用户,这个解决方案肯定会有效。

答案 2 :(得分:1)

您必须在获取后对用户列表进行排序。在您执行此操作之前,您必须向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:只有组织&#34; AAA&#34;
    • User2:只有组织&#34; AAAA&#34;
    • User3:有组织&#34; AAA&#34;和&#34; BBB&#34;
    • User4:有组织&#34; AAAA&#34;和&#34; BBB&#34;
  3. 使用string_agg(o.name, ',')对其进行排序后,我们会收到以下订单:

    • User1&#34; AAA&#34;
    • User2&#34; AAAA&#34;
    • User4&#34; AAAA,BBB&#34;
    • User3&#34; AAA,BBB&#34;

    这是不正确的。

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

    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字段对用户进行排序即可。您可以在该列上添加索引,这样的排序应该很快。它不需要任何连接或子查询。