Assuming I have classes User and UserGroup. There is an optional 1-many group to user association and the association is mapped from both sides (the UserGroup side via a property called "members", which is a HashSet, the User side via property "group").
Using the Criteria API, how can I query for all groups, sorted by count of group members?
(Edit) I should have pointed out when I asked this that pagination was being performed in the SQL, so the ordering should also be performed in the SQL if possible.
Another option would be to use the @Formula annotation which is basically the equivalent of creating a nested select so you can get at your counts.
On your DB objects property / member in question (which you should create), add the annotation to the getter like:
@Formula("(SELECT COUNT(TABLE_NAME.ID) FROM TABLE WHERE whatever...)")
public long getNewProperty{
return newProperty;
}
Then target the member directly to get hold of your count values...
Note: Be aware that when specifying the sql within the formula annotation, you must specify the field names directly when referencing the current objects table (this) as hibernate deals with this itself. So in your WHERE clause just use ID or whatever on its own when referencing the current objects table which I suspect is UserGroup.
I had to have a setter for my new property in order for hibernate to work and prevent the compiler from complaining.
There may be other clever ways of using @Formula, but I'm quite new to this and there doesn't appear to be very much documentation on the subject...
If you've never used nested SELECTS before, it might be an idea for you to just recreate in sql first - that helped me.
Hope this helps
It is not possible by default using Citeria API but you can extend org.hibernate.criterion.Order class.
This article is about how to extend that class: http://blog.tremend.ro/2008/06/10/how-to-order-by-a-custom-sql-formulaexpression-when-using-hibernate-criteria-api
My sollution is:
public class SizeOrder extends Order {
protected String propertyName;
protected boolean ascending;
protected SizeOrder(String propertyName, boolean ascending) {
super(propertyName, ascending);
this.propertyName = propertyName;
this.ascending = ascending;
}
public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException {
String role = criteriaQuery.getEntityName(criteria, propertyName) + '.' + criteriaQuery.getPropertyName(propertyName);
QueryableCollection cp = (QueryableCollection) criteriaQuery.getFactory().getCollectionPersister(role);
String[] fk = cp.getKeyColumnNames();
String[] pk = ((Loadable) cp.getOwnerEntityPersister())
.getIdentifierColumnNames();
return " (select count(*) from " + cp.getTableName() + " where "
+ new ConditionFragment()
.setTableAlias(
criteriaQuery.getSQLAlias(criteria, propertyName)
).setCondition(pk, fk)
.toFragmentString() + ") "
+ (ascending ? "asc" : "desc");
}
public static SizeOrder asc(String propertyName) {
return new SizeOrder(propertyName, true);
}
public static SizeOrder desc(String propertyName) {
return new SizeOrder(propertyName, false);
}
}
The toSqlString method is based on org.hibernate.criterion.SizeExpression class. (Source: http://javasourcecode.org/html/open-source/hibernate/hibernate-3.6.0.Final/org/hibernate/criterion/SizeExpression.java.html)
Example usage:
hibernateSession.createCriteria(UserGroup.class).addOrder( SizeOrder.asc("members") ).list();
this will list user groups ordered by the size of the members ascending, where "members" is the User collection in UserGroup entity.
You could probably so this by defining a "derived property" on your entity, which would be a read-only property which returns the size of the collection inside that entity. The easiest way to define a derived property is documented here:
update, insert (optional - defaults to
true): specifies that the mapped
columns should be included in SQL
UPDATE and/or INSERT statements.
Setting both to false allows a pure
"derived" property whose value is
initialized from some other property
that maps to the same column(s), or by
a trigger or other application.
Once the property is defined, you should be able to use the property name as an order clause in the criteria API, just like any other property. I haven't tried this myself, though.
If it works, it'll likely be done as an in-memory sort, since it won't be able to map it to SQL. If this is a problem, then the same link above documents how to implement a derived property using a custom SQL fragment:
A powerful feature is derived
properties. These properties are by
definition read-only. The property
value is computed at load time. You
declare the computation as an SQL
expression. This then translates to a
SELECT clause subquery in the SQL
query that loads an instance: