16

How can I, using the JPA criteria API do the following:

select count(distinct column1, column2) from table

Doing this on one column/path is simple using CriteriaBuilder.countDistinct, but how can I do this on two paths/columns?

Piotr
  • 4,813
  • 7
  • 35
  • 46
  • note: some dbms do not support this kind of syntax. i don't know if any does. AFAIK Hibernate doe not support this – Firo Feb 08 '12 at 16:05
  • Indeed that seems to be the problem. MySQL does but probably not very common – Piotr Feb 09 '12 at 14:42
  • did you find in the meantime any solution to your question? I am having the same situation and don't find a solution for it. – marius_neo Jan 09 '13 at 15:48
  • It seems there is no way to do this in JPA2 – Piotr Jan 17 '13 at 10:04
  • IN HQL you can do something like `select count(distinct a.column1) from table table1 a left join a.tableb b group by b.column2, a.column1` but the `exceuteQuery` result needs to gather `.sum()` of results – V H Aug 28 '18 at 10:10

4 Answers4

9

Here is a late answer :-) though I'm not sure if things had changed.

Recently I encountered the very same need, and worked around it using concat, i.e., by concatenating the columns into a pseudo column, then countDistinct on the pseudo column.

But I couldn't use criteriaBuilder.concat because it generated JPQL using || for the concatenation, which Hibernate had trouble with.

Fortunately there's @Formula, thus, I mapped the pseudo column to a field with @Formula:

@Entity
public class MyEntity {
  @Column(name="col_a")
  private String colA;

  @Column(name="col_b")
  private String colB;

  @Formula("concat(col_a, col_b)") // <= THE TRICK
  private String concated;
}

This way I can finally use the concated field for CriteriaBuilder.countDistinct:

//...
Expression<?> exp = criteriaBuilder.countDistinct(entity.get("concated"));
criteriaQuery.select(exp);

TypedQuery<Long> query = entityManager.createQuery(criteriaQuery);
return query.getSingleResult();

I wish JPA would (or hopefully already) support countDistinct with multiple columns, then all these mess could have been avoided.

Community
  • 1
  • 1
ryenus
  • 15,711
  • 5
  • 56
  • 63
  • Ah, yes that should work. Ugly hack but at least it works! :) – Piotr Nov 18 '14 at 23:11
  • I actually solved it using a native query and it will likely perform better than your solution, but I can see how your solution will work as well. – Piotr Nov 19 '14 at 06:05
  • I see, and yes, native query would definitely perform better, especially when considering how index would be used. My wish is still there and open. – ryenus Nov 19 '14 at 06:13
  • I rather prefer the above SQL functionwith variable arguments since this is generic. – djmj Sep 24 '21 at 20:35
1

You can use hibernate dialect for this task. To do this, create your own dialect, that extends dialect of DB what used (list of all dialects), and then register new function. For example, I use MySQL 5 with InnoDB engine:

public final class MyDialect extends MySQL5InnoDBDialect {
    public MyDialect() {
        super();
        registerFunction("pairCountDistinct", new SQLFunctionTemplate(LongType.INSTANCE, "count(distinct ?1, ?2)"));
    }
}

After then add new property in persistence.xml:

<property name="hibernate.dialect" value="com.example.dialect.MyDialect" />

And now you can use this function:

// some init actions
final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Long> criteria = builder.createQuery(Long.class);
final Root<SomeEntity> root = criteria.from(SomeEntity.class);
criteria.select(builder.function("pairCountDistinct", Long.class, root.get(SomeEntity_.field1), root.get(SomeEntity_.field2)));
final long result = entityManager.createQuery(criteria).getSingleResult();
// some close actions
svishevsky
  • 86
  • 1
  • 3
  • Nice idea and can be improved using `VarArgsSqlFunction` to be more flexible. https://stackoverflow.com/questions/39462384/how-can-get-count-of-rows-in-hibernate-when-hql-have-group-by/69320916#69320916 – djmj Sep 24 '21 at 20:44
1

As others have pointed out, build up an Expression consisting of all desired columns using the concat method, but then you can just pass that Expression to the countDistinct method.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Root<MyEntity> root = cb.from(MyEntity.class);
// build up an expression with multiple columns
Expression exp = root.get("column1");
exp = cb.concat(exp, root.get("column2");
exp = cb.concat(exp, root.get("column3");

cq.select(cb.contDistinct(exp));

var result = em.createQuery(cq).getSingleResult();
var count = Math.toIntExact(result);
Mike Ryan
  • 2,359
  • 2
  • 17
  • 24
1

Seems like there is no way to do this with JPA2

Piotr
  • 4,813
  • 7
  • 35
  • 46