Hibernate version:3b4
I played a bit with the new Criteria API and discovered some queer behaviors, particularly when it comes to DetachedCriteria and Subqueries.
As an example, let's consider a two level folder system (yes very simple) and say we want to fetch the folders a user has access to.
First level folders are parents. Second level folders are children.
Authorizations (always positive authorizations, no negative) may be granted at the parent or child level. Authorizations are inherited from a parent to its children.
The code example (see the end of the post) creates the following objects:
- parent folder
- child folder
- authorization object referencing the parent
The Criteria fetches the folders using a disjunction. The first disjunction element fetches the folders which have been granted access explicitely. The second disjunction element fetches folders whose parents have been granted access.
When I run this simple test, I get the following NullPointerException:
Code:
java.lang.NullPointerException
at org.hibernate.criterion.SubqueryExpression.getTypedValues(SubqueryExpression.java:73)
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getQueryParameters(CriteriaQueryTranslator.java:230)
at org.hibernate.criterion.SubqueryExpression.toSqlString(SubqueryExpression.java:50)
at org.hibernate.criterion.Junction.toSqlString(Junction.java:58)
at org.hibernate.loader.criteria.CriteriaQueryTranslator.getWhereCondition(CriteriaQueryTranslator.java:312)
at org.hibernate.loader.criteria.CriteriaLoader.<init>(CriteriaLoader.java:92)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1208)
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:299)
at test.subqueries.SubqueriesTest.main(SubqueriesTest.java:41)
Exception in thread "main"
To make it work, I had to do the following:
1/
update SubqueryExpression.getTypeValues(Criteria, CriteriaQuery) to initialize the params instance variable :
Code:
public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException {
if (params == null) { // FIXME copy-paste from toLeftSqlString()
final SessionFactoryImplementor factory = ( (CriteriaImpl) criteria ).getSession().getFactory();
final OuterJoinLoadable persister = (OuterJoinLoadable) factory.getEntityPersister( criteriaImpl.getEntityOrClassName() );
CriteriaQueryTranslator innerQuery = new CriteriaQueryTranslator(
factory,
criteriaImpl,
criteriaImpl.getEntityOrClassName(), //implicit polymorphism not supported (would need a union)
criteriaQuery.generateSQLAlias(),
criteriaQuery);
params = innerQuery.getQueryParameters(); //TODO: bad lifecycle....
}
Type[] types = params.getPositionalParameterTypes();
Object[] values = params.getPositionalParameterValues();
TypedValue[] tv = new TypedValue[types.length];
for (int i = 0; i < types.length; i++) {
tv[i] = new TypedValue(types[i], values[i]);
}
return tv;
}
2/
call getExecutableCriteria(Session) on each DetachedCriteria to have the CriteriaImpl initialized
3/ not mentionned before, I had to
force Projections on every DetachedCriteria used in a SubqueryExpression, even for ExistsSubqueryExpression expressions.
Then Hibernate fetches the two folders. The generated sql is:
Code:
Hibernate: select this_.folder_id as folder1_0_, this_.label as label0_0_, this_.parent_id as parent3_0_0_ from folder this_ where (exists (select this0__.authorization_id as y0_ from authorization this0__ where this0__.user_id=? and this0__.folder_id=this_.folder_id) or this_.parent_id in (select this0__.folder_id as y0_ from folder this0__ where this0__.folder_id=this_.parent_id and exists (select this0__.authorization_id as y0_ from authorization this0__ where this0__.user_id=? and this0__.folder_id=this0__.folder_id)))
Code and mappings required to reproduce the error;
workaround comments are part of the "solution" I found to make it work:
Code:
/*
* Created on Feb 22, 2005
*/
package test.subqueries;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Subqueries;
/**
* @author gg
*/
public class SubqueriesTest {
public static void main(String[] args) {
Session session = config();
User me = setupModel(session);
Criteria criteria = session.createCriteria(Folder.class, "root_alias");
String rootAlias = criteria.getAlias();
Disjunction disjunction = Restrictions.disjunction();
criteria.add(disjunction);
// detached criteria to evaluate directly attached authorizations:
DetachedCriteria localAuthCriteria = createAuthorizationCriteria(me, rootAlias);
localAuthCriteria.getExecutableCriteria(session); // WORKAROUND
disjunction.add(Subqueries.exists(localAuthCriteria));
// detached criteria to evaluate parent authorization inheritance:
DetachedCriteria authorization2 = DetachedCriteria.forClass(Folder.class, "parent_alias");
authorization2.add(Restrictions.eqProperty("id", rootAlias + ".parent.id"));
authorization2.getExecutableCriteria(session); // WORKAROUND
DetachedCriteria parentAuthCriteria = createAuthorizationCriteria(me, authorization2.getAlias());
parentAuthCriteria.getExecutableCriteria(session); // WORKAROUND
authorization2.add(Subqueries.exists(parentAuthCriteria));
authorization2.setProjection(Projections.property("id"));
disjunction.add(Subqueries.propertyIn("parent.id", authorization2));
List folders = criteria.list();
}
/**
* @param me
* @param rootAlias
* @return
*/
private static DetachedCriteria createAuthorizationCriteria(User me, String alias) {
DetachedCriteria criteria = DetachedCriteria.forClass(Authorization.class, alias + "_authorization");
criteria.add(Restrictions.eq("user", me));
criteria.add(Restrictions.eqProperty("folder.id", alias + ".id"));
criteria.setProjection(Projections.property("id"));
return criteria;
}
private static Session config() {
Configuration cfg = new Configuration().configure().addClass(Folder.class).addClass(User.class).addClass(Authorization.class);
SessionFactory sessions = cfg.buildSessionFactory();
return sessions.openSession();
}
private static User setupModel(Session session) {
Folder root = new Folder("root");
session.save(root);
Folder child = new Folder("child");
child.setParent(root);
session.save(child);
User user = new User("jerome");
session.save(user);
Authorization authorization = new Authorization(root, user);
session.save(authorization);
return user;
}
}
The mapping documents:
Code:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class
name="test.subqueries.Folder"
table="folder"
dynamic-update="false"
dynamic-insert="false"
>
<id
name="id"
column="folder_id"
type="long"
unsaved-value="null"
>
<generator class="identity">
</generator>
</id>
<set
name="children"
lazy="true"
inverse="false"
cascade="none"
sort="unsorted"
>
<key
column="parent_id"
/>
<one-to-many
class="test.subqueries.Folder"
/>
</set>
<property
name="label"
type="java.lang.String"
update="true"
insert="true"
column="label"
/>
<many-to-one
name="parent"
class="test.subqueries.Folder"
cascade="none"
outer-join="auto"
update="true"
insert="true"
column="parent_id"
/>
<!--
To add non XDoclet property mappings, create a file named
hibernate-properties-Folder.xml
containing the additional properties and place it in your merge dir.
-->
</class>
</hibernate-mapping>
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class
name="test.subqueries.User"
table="user"
dynamic-update="false"
dynamic-insert="false"
>
<id
name="id"
column="user_id"
type="long"
unsaved-value="null"
>
<generator class="identity">
</generator>
</id>
<property
name="label"
type="java.lang.String"
update="true"
insert="true"
column="label"
/>
<!--
To add non XDoclet property mappings, create a file named
hibernate-properties-User.xml
containing the additional properties and place it in your merge dir.
-->
</class>
</hibernate-mapping>
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class
name="test.subqueries.Authorization"
table="authorization"
dynamic-update="false"
dynamic-insert="false"
>
<id
name="id"
column="authorization_id"
type="long"
unsaved-value="null"
>
<generator class="identity">
</generator>
</id>
<many-to-one
name="folder"
class="test.subqueries.Folder"
cascade="none"
outer-join="auto"
update="true"
insert="true"
column="folder_id"
/>
<many-to-one
name="user"
class="test.subqueries.User"
cascade="none"
outer-join="auto"
update="true"
insert="true"
column="user_id"
/>
</class>
</hibernate-mapping>
The java classes:
Code:
/*
* Created on Feb 22, 2005
*/
package test.subqueries;
import java.util.Set;
/**
* @author gg
* @hibernate.class table = "folder"
*/
public class Folder {
private long id;
private String label;
private Folder parent;
private Set children;
public Folder() {
}
public Folder(String label) {
this.label = label;
}
/**
* @hibernate.set lazy = "true"
* @hibernate.collection-key column = "parent_id"
* @hibernate.collection-one-to-many class = "test.subqueries.Folder"
* @return
*/
public Set getChildren() {
return this.children;
}
public void setChildren(Set children) {
this.children = children;
}
/**
* @hibernate.id column = "folder_id"
* generator-class = "identity"
* unsaved-value = "null"
* @return
*/
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
/**
* @hibernate.property column = "label"
* @return
*/
public String getLabel() {
return this.label;
}
public void setLabel(String label) {
this.label = label;
}
/**
* @hibernate.many-to-one column = "parent_id"
* @return
*/
public Folder getParent() {
return this.parent;
}
public void setParent(Folder parent) {
this.parent = parent;
}
}
/*
* Created on Feb 22, 2005
*/
package test.subqueries;
/**
* @author gg
* @hibernate.class table = "user"
*/
public class User {
private long id;
private String label;
public User() {
}
public User(String label) {
this.label = label;
}
/**
* @hibernate.id column = "user_id"
* generator-class = "identity"
* unsaved-value = "null"
* @return
*/
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
/**
* @hibernate.property column = "label"
* @return
*/
public String getLabel() {
return this.label;
}
public void setLabel(String label) {
this.label = label;
}
}
/*
* Created on Feb 22, 2005
*/
package test.subqueries;
/**
* @author gg
* @hibernate.class table = "authorization"
*/
public class Authorization {
private long id;
private Folder folder;
private User user;
public Authorization() {
}
/**
* @hibernate.id column = "authorization_id"
* generator-class = "identity"
* unsaved-value = "null"
* @return
*/
public long getId() {
return this.id;
}
public void setId(long id) {
this.id = id;
}
public Authorization(Folder folder, User user) {
this.folder = folder;
this.user = user;
}
/**
* @hibernate.many-to-one column = "folder_id"
* @return
*/
public Folder getFolder() {
return this.folder;
}
public void setFolder(Folder folder) {
this.folder = folder;
}
/**
* @hibernate.many-to-one column = "user_id"
* @return
*/
public User getUser() {
return this.user;
}
public void setUser(User user) {
this.user = user;
}
}
Has anybody encountered similar problems with DetachedCriteria and subqueries ?
Cheers,
Jerome
PS: apart from these issues, the new Criteria API rocks ;)