From a8e34f0066126d9f5b504ea4c5dd408e7f98c8d3 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Fri, 20 Nov 2009 05:20:28 +0000 Subject: [PATCH] HHH-2308 - Adding predicates to the join condition using Criteria Query git-svn-id: https://svn.jboss.org/repos/hibernate/core/trunk@18012 1b8cb986-b30d-0410-93ca-fae66ebed9b2 --- .../src/main/java/org/hibernate/Criteria.java | 70 +++++++++++++- .../java/org/hibernate/impl/CriteriaImpl.java | 52 +++++++--- .../loader/AbstractEntityJoinWalker.java | 1 + .../java/org/hibernate/loader/JoinWalker.java | 5 + .../loader/OuterJoinableAssociation.java | 6 +- .../collection/BasicCollectionJoinWalker.java | 1 + .../collection/OneToManyJoinWalker.java | 1 + .../loader/criteria/CriteriaJoinWalker.java | 4 + .../criteria/CriteriaQueryTranslator.java | 73 +++++++++----- .../docbook/en-US/content/query_criteria.xml | 28 ++++++ .../test/criteria/CriteriaQueryTest.java | 94 +++++++++++++++++++ 11 files changed, 293 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/org/hibernate/Criteria.java b/core/src/main/java/org/hibernate/Criteria.java index 47a2eb1795..aa5a17e6b4 100644 --- a/core/src/main/java/org/hibernate/Criteria.java +++ b/core/src/main/java/org/hibernate/Criteria.java @@ -135,7 +135,10 @@ public interface Criteria extends CriteriaSpecification { * * @param associationPath a dot seperated property path * @param mode The fetch mode for the referenced association + * * @return this (for method chaining) + * + * @throws HibernateException Indicates a problem applying the given fetch mode */ public Criteria setFetchMode(String associationPath, FetchMode mode) throws HibernateException; @@ -143,6 +146,7 @@ public interface Criteria extends CriteriaSpecification { * Set the lock mode of the current entity * * @param lockMode The lock mode to be applied + * * @return this (for method chaining) */ public Criteria setLockMode(LockMode lockMode); @@ -151,8 +155,9 @@ public interface Criteria extends CriteriaSpecification { * Set the lock mode of the aliased entity * * @param alias The previously assigned alias representing the entity to - * which the given lock mode should apply. + * which the given lock mode should apply. * @param lockMode The lock mode to be applied + * * @return this (for method chaining) */ public Criteria setLockMode(String alias, LockMode lockMode); @@ -165,7 +170,10 @@ public interface Criteria extends CriteriaSpecification { * * @param associationPath A dot-seperated property path * @param alias The alias to assign to the joined association (for later reference). + * * @return this (for method chaining) + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createAlias(String associationPath, String alias) throws HibernateException; @@ -179,10 +187,31 @@ public interface Criteria extends CriteriaSpecification { * @param associationPath A dot-seperated property path * @param alias The alias to assign to the joined association (for later reference). * @param joinType The type of join to use. + * * @return this (for method chaining) + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createAlias(String associationPath, String alias, int joinType) throws HibernateException; + /** + * Join an association using the specified join-type, assigning an alias + * to the joined association. + *

+ * The joinType is expected to be one of {@link #INNER_JOIN} (the default), + * {@link #FULL_JOIN}, or {@link #LEFT_JOIN}. + * + * @param associationPath A dot-seperated property path + * @param alias The alias to assign to the joined association (for later reference). + * @param joinType The type of join to use. + * @param withClause The criteria to be added to the join condition (ON clause) + * + * @return this (for method chaining) + * + * @throws HibernateException Indicates a problem creating the sub criteria + */ + public Criteria createAlias(String associationPath, String alias, int joinType, Criterion withClause) throws HibernateException; + /** * Create a new Criteria, "rooted" at the associated entity. *

@@ -190,7 +219,10 @@ public interface Criteria extends CriteriaSpecification { * {@link #INNER_JOIN} for the joinType. * * @param associationPath A dot-seperated property path + * * @return the created "sub criteria" + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createCriteria(String associationPath) throws HibernateException; @@ -200,7 +232,10 @@ public interface Criteria extends CriteriaSpecification { * * @param associationPath A dot-seperated property path * @param joinType The type of join to use. + * * @return the created "sub criteria" + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createCriteria(String associationPath, int joinType) throws HibernateException; @@ -213,7 +248,10 @@ public interface Criteria extends CriteriaSpecification { * * @param associationPath A dot-seperated property path * @param alias The alias to assign to the joined association (for later reference). + * * @return the created "sub criteria" + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createCriteria(String associationPath, String alias) throws HibernateException; @@ -224,10 +262,28 @@ public interface Criteria extends CriteriaSpecification { * @param associationPath A dot-seperated property path * @param alias The alias to assign to the joined association (for later reference). * @param joinType The type of join to use. + * * @return the created "sub criteria" + * + * @throws HibernateException Indicates a problem creating the sub criteria */ public Criteria createCriteria(String associationPath, String alias, int joinType) throws HibernateException; + /** + * Create a new Criteria, "rooted" at the associated entity, + * assigning the given alias and using the specified join type. + * + * @param associationPath A dot-seperated property path + * @param alias The alias to assign to the joined association (for later reference). + * @param joinType The type of join to use. + * @param withClause The criteria to be added to the join condition (ON clause) + * + * @return the created "sub criteria" + * + * @throws HibernateException Indicates a problem creating the sub criteria + */ + public Criteria createCriteria(String associationPath, String alias, int joinType, Criterion withClause) throws HibernateException; + /** * Set a strategy for handling the query results. This determines the * "shape" of the query result. @@ -327,6 +383,9 @@ public interface Criteria extends CriteriaSpecification { * Get the results. * * @return The list of matched query results. + * + * @throws HibernateException Indicates a problem either translating the criteria to SQL, + * exeucting the SQL or processing the SQL results. */ public List list() throws HibernateException; @@ -335,6 +394,9 @@ public interface Criteria extends CriteriaSpecification { * * @return The {@link ScrollableResults} representing the matched * query results. + * + * @throws HibernateException Indicates a problem either translating the criteria to SQL, + * exeucting the SQL or processing the SQL results. */ public ScrollableResults scroll() throws HibernateException; @@ -344,8 +406,12 @@ public interface Criteria extends CriteriaSpecification { * * @param scrollMode Indicates the type of underlying database cursor to * request. + * * @return The {@link ScrollableResults} representing the matched * query results. + * + * @throws HibernateException Indicates a problem either translating the criteria to SQL, + * exeucting the SQL or processing the SQL results. */ public ScrollableResults scroll(ScrollMode scrollMode) throws HibernateException; @@ -358,4 +424,4 @@ public interface Criteria extends CriteriaSpecification { */ public Object uniqueResult() throws HibernateException; -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hibernate/impl/CriteriaImpl.java b/core/src/main/java/org/hibernate/impl/CriteriaImpl.java index 5c3f8d1d5f..6568d64744 100644 --- a/core/src/main/java/org/hibernate/impl/CriteriaImpl.java +++ b/core/src/main/java/org/hibernate/impl/CriteriaImpl.java @@ -80,7 +80,7 @@ public class CriteriaImpl implements Criteria, Serializable { private CacheMode cacheMode; private FlushMode sessionFlushMode; private CacheMode sessionCacheMode; - + private ResultTransformer resultTransformer = Criteria.ROOT_ENTITY; @@ -202,6 +202,11 @@ public class CriteriaImpl implements Criteria, Serializable { return this; } + public Criteria createAlias(String associationPath, String alias, int joinType, Criterion withClause) { + new Subcriteria( this, associationPath, alias, joinType, withClause ); + return this; + } + public Criteria createCriteria(String associationPath) { return createCriteria( associationPath, INNER_JOIN ); } @@ -218,6 +223,10 @@ public class CriteriaImpl implements Criteria, Serializable { return new Subcriteria( this, associationPath, alias, joinType ); } + public Criteria createCriteria(String associationPath, String alias, int joinType, Criterion withClause) { + return new Subcriteria( this, associationPath, alias, joinType, withClause ); + } + public ResultTransformer getResultTransformer() { return resultTransformer; } @@ -309,7 +318,7 @@ public class CriteriaImpl implements Criteria, Serializable { after(); } } - + public ScrollableResults scroll() { return scroll( ScrollMode.SCROLL_INSENSITIVE ); } @@ -338,7 +347,7 @@ public class CriteriaImpl implements Criteria, Serializable { getSession().setCacheMode( cacheMode ); } } - + protected void after() { if ( sessionFlushMode != null ) { getSession().setFlushMode( sessionFlushMode ); @@ -349,7 +358,7 @@ public class CriteriaImpl implements Criteria, Serializable { sessionCacheMode = null; } } - + public boolean isLookupByNaturalKey() { if ( projection != null ) { return false; @@ -374,16 +383,21 @@ public class CriteriaImpl implements Criteria, Serializable { private Criteria parent; private LockMode lockMode; private int joinType; - + private Criterion withClause; // Constructors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - private Subcriteria(Criteria parent, String path, String alias, int joinType) { + private Subcriteria(Criteria parent, String path, String alias, int joinType, Criterion withClause) { this.alias = alias; this.path = path; this.parent = parent; this.joinType = joinType; - CriteriaImpl.this.subcriteriaList.add(this); + this.withClause = withClause; + CriteriaImpl.this.subcriteriaList.add( this ); + } + + private Subcriteria(Criteria parent, String path, String alias, int joinType) { + this( parent, path, alias, joinType, null ); } private Subcriteria(Criteria parent, String path, int joinType) { @@ -391,10 +405,10 @@ public class CriteriaImpl implements Criteria, Serializable { } public String toString() { - return "Subcriteria(" + - path + ":" + - (alias==null ? "" : alias) + - ')'; + return "Subcriteria(" + + path + ":" + + (alias==null ? "" : alias) + + ')'; } @@ -429,6 +443,10 @@ public class CriteriaImpl implements Criteria, Serializable { return joinType; } + public Criterion getWithClause() { + return this.withClause; + } + // Criteria impl ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -451,6 +469,11 @@ public class CriteriaImpl implements Criteria, Serializable { return this; } + public Criteria createAlias(String associationPath, String alias, int joinType, Criterion withClause) throws HibernateException { + new Subcriteria( this, associationPath, alias, joinType, withClause ); + return this; + } + public Criteria createCriteria(String associationPath) { return createCriteria( associationPath, INNER_JOIN ); } @@ -467,6 +490,10 @@ public class CriteriaImpl implements Criteria, Serializable { return new Subcriteria( Subcriteria.this, associationPath, alias, joinType ); } + public Criteria createCriteria(String associationPath, String alias, int joinType, Criterion withClause) throws HibernateException { + return new Subcriteria( this, associationPath, alias, joinType, withClause ); + } + public Criteria setCacheable(boolean cacheable) { CriteriaImpl.this.setCacheable(cacheable); return this; @@ -493,8 +520,7 @@ public class CriteriaImpl implements Criteria, Serializable { return CriteriaImpl.this.uniqueResult(); } - public Criteria setFetchMode(String associationPath, FetchMode mode) - throws HibernateException { + public Criteria setFetchMode(String associationPath, FetchMode mode) { CriteriaImpl.this.setFetchMode( StringHelper.qualify(path, associationPath), mode); return this; } diff --git a/core/src/main/java/org/hibernate/loader/AbstractEntityJoinWalker.java b/core/src/main/java/org/hibernate/loader/AbstractEntityJoinWalker.java index 1d5af1b347..4f2db2ae99 100755 --- a/core/src/main/java/org/hibernate/loader/AbstractEntityJoinWalker.java +++ b/core/src/main/java/org/hibernate/loader/AbstractEntityJoinWalker.java @@ -85,6 +85,7 @@ public abstract class AbstractEntityJoinWalker extends JoinWalker { null, alias, JoinFragment.LEFT_OUTER_JOIN, + null, getFactory(), CollectionHelper.EMPTY_MAP ) diff --git a/core/src/main/java/org/hibernate/loader/JoinWalker.java b/core/src/main/java/org/hibernate/loader/JoinWalker.java index 9977916308..471160d59d 100755 --- a/core/src/main/java/org/hibernate/loader/JoinWalker.java +++ b/core/src/main/java/org/hibernate/loader/JoinWalker.java @@ -207,6 +207,10 @@ public class JoinWalker { } } + protected String getWithClause(String path) { + return ""; + } + /** * Add on association (one-to-one, many-to-one, or a collection) to a list * of associations to be fetched by outerjoin @@ -235,6 +239,7 @@ public class JoinWalker { aliasedLhsColumns, subalias, joinType, + getWithClause(path), getFactory(), loadQueryInfluencers.getEnabledFilters() ); diff --git a/core/src/main/java/org/hibernate/loader/OuterJoinableAssociation.java b/core/src/main/java/org/hibernate/loader/OuterJoinableAssociation.java index dfa7cc8dd4..5e22410cbb 100644 --- a/core/src/main/java/org/hibernate/loader/OuterJoinableAssociation.java +++ b/core/src/main/java/org/hibernate/loader/OuterJoinableAssociation.java @@ -59,6 +59,7 @@ public final class OuterJoinableAssociation { String[] lhsColumns, String rhsAlias, int joinType, + String withClause, SessionFactoryImplementor factory, Map enabledFilters) throws MappingException { this.joinableType = joinableType; @@ -68,7 +69,8 @@ public final class OuterJoinableAssociation { this.joinType = joinType; this.joinable = joinableType.getAssociatedJoinable(factory); this.rhsColumns = JoinHelper.getRHSColumnNames(joinableType, factory); - this.on = joinableType.getOnCondition(rhsAlias, factory, enabledFilters); + this.on = joinableType.getOnCondition(rhsAlias, factory, enabledFilters) + + ( withClause == null || withClause.trim().length() == 0 ? "" : " and ( " + withClause + " )" ); this.enabledFilters = enabledFilters; // needed later for many-to-many/filter application } @@ -188,4 +190,4 @@ public final class OuterJoinableAssociation { joinable.whereJoinFragment(rhsAlias, false, true) ); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/hibernate/loader/collection/BasicCollectionJoinWalker.java b/core/src/main/java/org/hibernate/loader/collection/BasicCollectionJoinWalker.java index 5e03750ea2..336fbc168e 100755 --- a/core/src/main/java/org/hibernate/loader/collection/BasicCollectionJoinWalker.java +++ b/core/src/main/java/org/hibernate/loader/collection/BasicCollectionJoinWalker.java @@ -80,6 +80,7 @@ public class BasicCollectionJoinWalker extends CollectionJoinWalker { null, alias, JoinFragment.LEFT_OUTER_JOIN, + null, getFactory(), CollectionHelper.EMPTY_MAP ) diff --git a/core/src/main/java/org/hibernate/loader/collection/OneToManyJoinWalker.java b/core/src/main/java/org/hibernate/loader/collection/OneToManyJoinWalker.java index 9acbec5583..325ed07885 100755 --- a/core/src/main/java/org/hibernate/loader/collection/OneToManyJoinWalker.java +++ b/core/src/main/java/org/hibernate/loader/collection/OneToManyJoinWalker.java @@ -85,6 +85,7 @@ public class OneToManyJoinWalker extends CollectionJoinWalker { null, alias, JoinFragment.LEFT_OUTER_JOIN, + null, getFactory(), CollectionHelper.EMPTY_MAP ) ); diff --git a/core/src/main/java/org/hibernate/loader/criteria/CriteriaJoinWalker.java b/core/src/main/java/org/hibernate/loader/criteria/CriteriaJoinWalker.java index afb1964911..2e84bb5c3f 100755 --- a/core/src/main/java/org/hibernate/loader/criteria/CriteriaJoinWalker.java +++ b/core/src/main/java/org/hibernate/loader/criteria/CriteriaJoinWalker.java @@ -210,4 +210,8 @@ public class CriteriaJoinWalker extends AbstractEntityJoinWalker { return "criteria query"; } + protected String getWithClause(String path) { + return translator.getWithClause(path); + } + } diff --git a/core/src/main/java/org/hibernate/loader/criteria/CriteriaQueryTranslator.java b/core/src/main/java/org/hibernate/loader/criteria/CriteriaQueryTranslator.java index 688015fe37..f1b8b69257 100755 --- a/core/src/main/java/org/hibernate/loader/criteria/CriteriaQueryTranslator.java +++ b/core/src/main/java/org/hibernate/loader/criteria/CriteriaQueryTranslator.java @@ -43,6 +43,7 @@ import org.hibernate.MappingException; import org.hibernate.QueryException; import org.hibernate.hql.ast.util.SessionFactoryHelper; import org.hibernate.criterion.CriteriaQuery; +import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Projection; import org.hibernate.engine.QueryParameters; import org.hibernate.engine.RowSelection; @@ -77,7 +78,8 @@ public class CriteriaQueryTranslator implements CriteriaQuery { private final Map aliasCriteriaMap = new HashMap(); private final Map associationPathCriteriaMap = new LinkedHashMap(); private final Map associationPathJoinTypesMap = new LinkedHashMap(); - + private final Map withClauseMap = new HashMap(); + private final SessionFactoryImplementor sessionFactory; public CriteriaQueryTranslator( @@ -169,6 +171,10 @@ public class CriteriaQueryTranslator implements CriteriaQuery { // TODO : not so sure this is needed... throw new QueryException( "duplicate association path: " + wholeAssociationPath ); } + if ( crit.getWithClause() != null ) + { + this.withClauseMap.put(wholeAssociationPath, crit.getWithClause()); + } } } @@ -266,9 +272,42 @@ public class CriteriaQueryTranslator implements CriteriaQuery { } public QueryParameters getQueryParameters() { + RowSelection selection = new RowSelection(); + selection.setFirstRow( rootCriteria.getFirstResult() ); + selection.setMaxRows( rootCriteria.getMaxResults() ); + selection.setTimeout( rootCriteria.getTimeout() ); + selection.setFetchSize( rootCriteria.getFetchSize() ); + + Map lockModes = new HashMap(); + Iterator iter = rootCriteria.getLockModes().entrySet().iterator(); + while ( iter.hasNext() ) { + Map.Entry me = ( Map.Entry ) iter.next(); + final Criteria subcriteria = getAliasedCriteria( ( String ) me.getKey() ); + lockModes.put( getSQLAlias( subcriteria ), me.getValue() ); + } List values = new ArrayList(); List types = new ArrayList(); - Iterator iter = rootCriteria.iterateExpressionEntries(); + iter = rootCriteria.iterateSubcriteria(); + while ( iter.hasNext() ) { + CriteriaImpl.Subcriteria subcriteria = ( CriteriaImpl.Subcriteria ) iter.next(); + LockMode lm = subcriteria.getLockMode(); + if ( lm != null ) { + lockModes.put( getSQLAlias( subcriteria ), lm ); + } + if ( subcriteria.getWithClause() != null ) + { + TypedValue[] tv = subcriteria.getWithClause().getTypedValues( subcriteria, this ); + for ( int i = 0; i < tv.length; i++ ) { + values.add( tv[i].getValue() ); + types.add( tv[i].getType() ); + } + } + } + + // Type and value gathering for the WHERE clause needs to come AFTER lock mode gathering, + // because the lock mode gathering loop now contains join clauses which can contain + // parameter bindings (as in the HQL WITH clause). + iter = rootCriteria.iterateExpressionEntries(); while ( iter.hasNext() ) { CriteriaImpl.CriterionEntry ce = ( CriteriaImpl.CriterionEntry ) iter.next(); TypedValue[] tv = ce.getCriterion().getTypedValues( ce.getCriteria(), this ); @@ -277,31 +316,9 @@ public class CriteriaQueryTranslator implements CriteriaQuery { types.add( tv[i].getType() ); } } + Object[] valueArray = values.toArray(); Type[] typeArray = ArrayHelper.toTypeArray( types ); - - RowSelection selection = new RowSelection(); - selection.setFirstRow( rootCriteria.getFirstResult() ); - selection.setMaxRows( rootCriteria.getMaxResults() ); - selection.setTimeout( rootCriteria.getTimeout() ); - selection.setFetchSize( rootCriteria.getFetchSize() ); - - Map lockModes = new HashMap(); - iter = rootCriteria.getLockModes().entrySet().iterator(); - while ( iter.hasNext() ) { - Map.Entry me = ( Map.Entry ) iter.next(); - final Criteria subcriteria = getAliasedCriteria( ( String ) me.getKey() ); - lockModes.put( getSQLAlias( subcriteria ), me.getValue() ); - } - iter = rootCriteria.iterateSubcriteria(); - while ( iter.hasNext() ) { - CriteriaImpl.Subcriteria subcriteria = ( CriteriaImpl.Subcriteria ) iter.next(); - LockMode lm = subcriteria.getLockMode(); - if ( lm != null ) { - lockModes.put( getSQLAlias( subcriteria ), lm ); - } - } - return new QueryParameters( typeArray, valueArray, @@ -576,4 +593,10 @@ public class CriteriaQueryTranslator implements CriteriaQuery { return propertyName; } + public String getWithClause(String path) + { + final Criterion crit = (Criterion)this.withClauseMap.get(path); + return crit == null ? null : crit.toSqlString(getCriteria(path), this); + } + } diff --git a/documentation/manual/src/main/docbook/en-US/content/query_criteria.xml b/documentation/manual/src/main/docbook/en-US/content/query_criteria.xml index 674894f14e..43b050f421 100644 --- a/documentation/manual/src/main/docbook/en-US/content/query_criteria.xml +++ b/documentation/manual/src/main/docbook/en-US/content/query_criteria.xml @@ -196,6 +196,34 @@ while ( iter.hasNext() ) { Cat kitten = (Cat) map.get("kt"); }]]> + + Additionally you may manipulate the result set using a left outer join: + + + + + This will return all of the Cats with a mate whose name starts with "good" + ordered by their mate's age, and all cats who do not have a mate. + This is useful when there is a need to order or limit in the database + prior to returning complex/large result sets, and removes many instances where + multiple queries would have to be performed and the results unioned + by java in memory. + + + Without this feature, first all of the cats without a mate would need to be loaded in one query. + + + A second query would need to retreive the cats with mates who's name started with "good" sorted by the mates age. + + + Thirdly, in memory; the lists would need to be joined manually. + diff --git a/testsuite/src/test/java/org/hibernate/test/criteria/CriteriaQueryTest.java b/testsuite/src/test/java/org/hibernate/test/criteria/CriteriaQueryTest.java index bb82a24aa2..ef464affe7 100755 --- a/testsuite/src/test/java/org/hibernate/test/criteria/CriteriaQueryTest.java +++ b/testsuite/src/test/java/org/hibernate/test/criteria/CriteriaQueryTest.java @@ -794,5 +794,99 @@ public class CriteriaQueryTest extends FunctionalTestCase { t.commit(); session.close(); } + + public void testAliasJoinCriterion() { + Session session = openSession(); + Transaction t = session.beginTransaction(); + + Course courseA = new Course(); + courseA.setCourseCode("HIB-A"); + courseA.setDescription("Hibernate Training A"); + session.persist(courseA); + + Course courseB = new Course(); + courseB.setCourseCode("HIB-B"); + courseB.setDescription("Hibernate Training B"); + session.persist(courseB); + + Student gavin = new Student(); + gavin.setName("Gavin King"); + gavin.setStudentNumber(232); + gavin.setPreferredCourse(courseA); + session.persist(gavin); + + Student leonardo = new Student(); + leonardo.setName("Leonardo Quijano"); + leonardo.setStudentNumber(233); + leonardo.setPreferredCourse(courseB); + session.persist(leonardo); + + Student johnDoe = new Student(); + johnDoe.setName("John Doe"); + johnDoe.setStudentNumber(235); + johnDoe.setPreferredCourse(null); + session.persist(johnDoe); + + // test == on one value exists + List result = session.createCriteria( Student.class ) + .createAlias( "preferredCourse", "pc", Criteria.LEFT_JOIN, Restrictions.eq("pc.courseCode", "HIB-A") ) + .setProjection( Property.forName("pc.courseCode") ) + .addOrder(Order.asc("pc.courseCode")) + .list(); + + assertEquals( 3, result.size() ); + + // can't be sure of NULL comparison ordering aside from they should + // either come first or last + if ( result.get( 0 ) == null ) { + assertNull(result.get(1)); + assertEquals( "HIB-A", result.get(2) ); + } + else { + assertNull( result.get(2) ); + assertNull( result.get(1) ); + assertEquals( "HIB-A", result.get(0) ); + } + + // test == on non existent value + result = session.createCriteria( Student.class ) + .createAlias( "preferredCourse", "pc", Criteria.LEFT_JOIN, Restrictions.eq("pc.courseCode", "HIB-R") ) + .setProjection( Property.forName("pc.courseCode") ) + .addOrder(Order.asc("pc.courseCode")) + .list(); + + assertEquals( 3, result.size() ); + assertNull( result.get(2) ); + assertNull( result.get(1) ); + assertNull(result.get(0) ); + + // test != on one existing value + result = session.createCriteria( Student.class ) + .createAlias( "preferredCourse", "pc", Criteria.LEFT_JOIN, Restrictions.ne("pc.courseCode", "HIB-A") ) + .setProjection( Property.forName("pc.courseCode") ) + .addOrder(Order.asc("pc.courseCode")) + .list(); + + assertEquals( 3, result.size() ); + // can't be sure of NULL comparison ordering aside from they should + // either come first or last + if ( result.get( 0 ) == null ) { + assertNull( result.get(1) ); + assertEquals( "HIB-B", result.get(2) ); + } + else { + assertEquals( "HIB-B", result.get(0) ); + assertNull( result.get(1) ); + assertNull( result.get(2) ); + } + + session.delete(gavin); + session.delete(leonardo); + session.delete(johnDoe); + session.delete(courseA); + session.delete(courseB); + t.commit(); + session.close(); + } }