diff --git a/documentation/src/main/docbook/devguide/en-US/appendices/legacy_criteria/Legacy_Criteria.xml b/documentation/src/main/docbook/devguide/en-US/appendices/legacy_criteria/Legacy_Criteria.xml
index 1cbe9496ff..7a27fb6851 100644
--- a/documentation/src/main/docbook/devguide/en-US/appendices/legacy_criteria/Legacy_Criteria.xml
+++ b/documentation/src/main/docbook/devguide/en-US/appendices/legacy_criteria/Legacy_Criteria.xml
@@ -119,7 +119,7 @@ List cats = sess.createCriteria(Cat.class)
diff --git a/documentation/src/main/docbook/devguide/en-US/appendix-Configuration_Properties.xml b/documentation/src/main/docbook/devguide/en-US/appendix-Configuration_Properties.xml
index 52cf44a5bd..280bcf1a91 100644
--- a/documentation/src/main/docbook/devguide/en-US/appendix-Configuration_Properties.xml
+++ b/documentation/src/main/docbook/devguide/en-US/appendix-Configuration_Properties.xml
@@ -77,6 +77,12 @@
Forces Hibernate to order SQL updates by the primary key value of the items being updated. This
reduces the likelihood of transaction deadlocks in highly-concurrent systems.
+
+ hibernate.order_by.default_null_ordering
+ none, first or last
+ Defines precedence of null values in ORDER BY clause. Defaults to
+ none which varies between RDBMS implementation.
+
hibernate.generate_statistics
true or false
diff --git a/documentation/src/main/docbook/devguide/en-US/chapters/query_ql/HQL_JPQL.xml b/documentation/src/main/docbook/devguide/en-US/chapters/query_ql/HQL_JPQL.xml
index 5aaf71c6ae..ad4006c732 100644
--- a/documentation/src/main/docbook/devguide/en-US/chapters/query_ql/HQL_JPQL.xml
+++ b/documentation/src/main/docbook/devguide/en-US/chapters/query_ql/HQL_JPQL.xml
@@ -1427,7 +1427,9 @@
Individual expressions in the order-by can be qualified with either ASC (ascending) or
- DESC (descending) to indicated the desired ordering direction.
+ DESC (descending) to indicated the desired ordering direction. Null values can be placed
+ in front or at the end of sorted set using NULLS FIRST or NULLS LAST
+ clause respectively.
Order-by examples
diff --git a/documentation/src/main/docbook/manual/en-US/content/collection_mapping.xml b/documentation/src/main/docbook/manual/en-US/content/collection_mapping.xml
index faebbd1cb3..3fe09b797d 100644
--- a/documentation/src/main/docbook/manual/en-US/content/collection_mapping.xml
+++ b/documentation/src/main/docbook/manual/en-US/content/collection_mapping.xml
@@ -483,8 +483,8 @@ public class Part {
@javax.persistence.OrderBy to your property. This
annotation takes as parameter a list of comma separated properties (of
the target entity) and orders the collection accordingly (eg
- firstname asc, age desc
), if the string is empty, the
- collection will be ordered by the primary key of the target
+ firstname asc, age desc, weight asc nulls last
), if the string
+ is empty, the collection will be ordered by the primary key of the target
entity.
diff --git a/documentation/src/main/docbook/manual/en-US/content/query_hql.xml b/documentation/src/main/docbook/manual/en-US/content/query_hql.xml
index c020cc4729..f1f4bce546 100644
--- a/documentation/src/main/docbook/manual/en-US/content/query_hql.xml
+++ b/documentation/src/main/docbook/manual/en-US/content/query_hql.xml
@@ -855,12 +855,17 @@ WHERE prod.name = 'widget'
+order by cat.name asc, cat.weight desc nulls first, cat.birthdate]]>
The optional asc or desc indicate ascending or descending order
respectively.
+
+
+ The optional nulls first or nulls last indicate precedence of null
+ values while sorting.
+
diff --git a/hibernate-core/src/main/antlr/hql-sql.g b/hibernate-core/src/main/antlr/hql-sql.g
index 80af06eb79..fbeb556da7 100644
--- a/hibernate-core/src/main/antlr/hql-sql.g
+++ b/hibernate-core/src/main/antlr/hql-sql.g
@@ -348,9 +348,18 @@ orderClause
;
orderExprs
- : orderExpr ( ASCENDING | DESCENDING )? (orderExprs)?
+ : orderExpr ( ASCENDING | DESCENDING )? ( nullOrdering )? (orderExprs)?
;
+nullOrdering
+ : NULLS nullPrecedence
+ ;
+
+nullPrecedence
+ : FIRST
+ | LAST
+ ;
+
orderExpr
: { isOrderExpressionResultVariableRef( _t ) }? resultVariableRef
| expr
diff --git a/hibernate-core/src/main/antlr/hql.g b/hibernate-core/src/main/antlr/hql.g
index 9289b0e070..4a7b98e50d 100644
--- a/hibernate-core/src/main/antlr/hql.g
+++ b/hibernate-core/src/main/antlr/hql.g
@@ -78,6 +78,9 @@ tokens
UPDATE="update";
VERSIONED="versioned";
WHERE="where";
+ NULLS="nulls";
+ FIRST;
+ LAST;
// -- SQL tokens --
// These aren't part of HQL, but the SQL fragment parser uses the HQL lexer, so they need to be declared here.
@@ -399,7 +402,7 @@ orderByClause
;
orderElement
- : expression ( ascendingOrDescending )?
+ : expression ( ascendingOrDescending )? ( nullOrdering )?
;
ascendingOrDescending
@@ -407,6 +410,24 @@ ascendingOrDescending
| ( "desc" | "descending") { #ascendingOrDescending.setType(DESCENDING); }
;
+nullOrdering
+ : NULLS nullPrecedence
+ ;
+
+nullPrecedence
+ : IDENT {
+ if ( "first".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
+ #nullPrecedence.setType( FIRST );
+ }
+ else if ( "last".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
+ #nullPrecedence.setType( LAST );
+ }
+ else {
+ throw new SemanticException( "Expecting 'first' or 'last', but found '" + #nullPrecedence.getText() + "' as null ordering precedence." );
+ }
+ }
+ ;
+
//## havingClause:
//## HAVING logicalExpression;
diff --git a/hibernate-core/src/main/antlr/order-by-render.g b/hibernate-core/src/main/antlr/order-by-render.g
index f4e7fe4e3f..fd8f9f82af 100644
--- a/hibernate-core/src/main/antlr/order-by-render.g
+++ b/hibernate-core/src/main/antlr/order-by-render.g
@@ -30,6 +30,7 @@ package org.hibernate.sql.ordering.antlr;
* Antlr grammar for rendering ORDER_BY trees as described by the {@link OrderByFragmentParser}
* @author Steve Ebersole
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
class GeneratedOrderByFragmentRenderer extends TreeParser;
@@ -53,6 +54,13 @@ options {
/*package*/ String getRenderedFragment() {
return buffer.toString();
}
+
+ /**
+ * Implementation note: This is just a stub. OrderByFragmentRenderer contains the effective implementation.
+ */
+ protected String renderOrderByElement(String expression, String collation, String order, String nulls) {
+ throw new UnsupportedOperationException("Concrete ORDER BY renderer should override this method.");
+ }
}
orderByFragment
@@ -61,32 +69,29 @@ orderByFragment
)
;
-sortSpecification
+sortSpecification { String sortKeySpec = null; String collSpec = null; String ordSpec = null; String nullOrd = null; }
: #(
- SORT_SPEC sortKeySpecification (collationSpecification)? (orderingSpecification)?
+ SORT_SPEC sortKeySpec=sortKeySpecification (collSpec=collationSpecification)? (ordSpec=orderingSpecification)? (nullOrd=nullOrdering)?
+ { out( renderOrderByElement( sortKeySpec, collSpec, ordSpec, nullOrd ) ); }
)
;
-sortKeySpecification
- : #(SORT_KEY sortKey)
+sortKeySpecification returns [String sortKeyExp = null]
+ : #(SORT_KEY s:sortKey) { sortKeyExp = #s.getText(); }
;
sortKey
- : i:IDENT {
- out( #i );
- }
+ : IDENT
;
-collationSpecification
- : c:COLLATE {
- out( " collate " );
- out( c );
- }
+collationSpecification returns [String collSpecExp = null]
+ : c:COLLATE { collSpecExp = "collate " + #c.getText(); }
;
-orderingSpecification
- : o:ORDER_SPEC {
- out( " " );
- out( #o );
- }
+orderingSpecification returns [String ordSpecExp = null]
+ : o:ORDER_SPEC { ordSpecExp = #o.getText(); }
+ ;
+
+nullOrdering returns [String nullOrdExp = null]
+ : n:NULL_ORDER { nullOrdExp = #n.getText(); }
;
\ No newline at end of file
diff --git a/hibernate-core/src/main/antlr/order-by.g b/hibernate-core/src/main/antlr/order-by.g
index dc141d1fe6..33ecbcccf1 100644
--- a/hibernate-core/src/main/antlr/order-by.g
+++ b/hibernate-core/src/main/antlr/order-by.g
@@ -46,6 +46,7 @@ tokens
ORDER_BY;
SORT_SPEC;
ORDER_SPEC;
+ NULL_ORDER;
SORT_KEY;
EXPR_LIST;
DOT;
@@ -55,6 +56,9 @@ tokens
COLLATE="collate";
ASCENDING="asc";
DESCENDING="desc";
+ NULLS="nulls";
+ FIRST;
+ LAST;
}
@@ -76,7 +80,7 @@ tokens
* @return The text.
*/
protected final String extractText(AST ast) {
- // for some reason, within AST creation blocks "[]" I am somtimes unable to refer to the AST.getText() method
+ // for some reason, within AST creation blocks "[]" I am sometimes unable to refer to the AST.getText() method
// using #var (the #var is not interpreted as the rule's output AST).
return ast.getText();
}
@@ -168,7 +172,7 @@ orderByFragment { trace("orderByFragment"); }
* the results should be sorted.
*/
sortSpecification { trace("sortSpecification"); }
- : sortKey (collationSpecification)? (orderingSpecification)? {
+ : sortKey (collationSpecification)? (orderingSpecification)? (nullOrdering)? {
#sortSpecification = #( [SORT_SPEC, "{sort specification}"], #sortSpecification );
#sortSpecification = postProcessSortSpecification( #sortSpecification );
}
@@ -290,6 +294,30 @@ orderingSpecification! { trace("orderingSpecification"); }
}
;
+/**
+ * Recognition rule for what SQL-2003 terms the null ordering; NULLS FIRST or
+ * NULLS LAST.
+ */
+nullOrdering! { trace("nullOrdering"); }
+ : NULLS n:nullPrecedence {
+ #nullOrdering = #( [NULL_ORDER, extractText( #n )] );
+ }
+ ;
+
+nullPrecedence { trace("nullPrecedence"); }
+ : IDENT {
+ if ( "first".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
+ #nullPrecedence.setType( FIRST );
+ }
+ else if ( "last".equalsIgnoreCase( #nullPrecedence.getText() ) ) {
+ #nullPrecedence.setType( LAST );
+ }
+ else {
+ throw new SemanticException( "Expecting 'first' or 'last', but found '" + #nullPrecedence.getText() + "' as null ordering precedence." );
+ }
+ }
+ ;
+
/**
* A simple-property-path is an IDENT followed by one or more (DOT IDENT) sequences
*/
diff --git a/hibernate-core/src/main/antlr/sql-gen.g b/hibernate-core/src/main/antlr/sql-gen.g
index 4abb0cfbfc..2bbfec192b 100644
--- a/hibernate-core/src/main/antlr/sql-gen.g
+++ b/hibernate-core/src/main/antlr/sql-gen.g
@@ -27,8 +27,11 @@ options {
/** the buffer resulting SQL statement is written to */
private StringBuilder buf = new StringBuilder();
+ private boolean captureExpression = false;
+ private StringBuilder expr = new StringBuilder();
+
protected void out(String s) {
- buf.append(s);
+ getStringBuilder().append( s );
}
/**
@@ -72,7 +75,7 @@ options {
}
protected StringBuilder getStringBuilder() {
- return buf;
+ return captureExpression ? expr : buf;
}
protected void nyi(AST n) {
@@ -92,6 +95,27 @@ options {
protected void commaBetweenParameters(String comma) {
out(comma);
}
+
+ protected void captureExpressionStart() {
+ captureExpression = true;
+ }
+
+ protected void captureExpressionFinish() {
+ captureExpression = false;
+ }
+
+ protected String resetCapture() {
+ final String expression = expr.toString();
+ expr = new StringBuilder();
+ return expression;
+ }
+
+ /**
+ * Implementation note: This is just a stub. SqlGenerator contains the effective implementation.
+ */
+ protected String renderOrderByElement(String expression, String order, String nulls) {
+ throw new UnsupportedOperationException("Concrete SQL generator should override this method.");
+ }
}
statement
@@ -152,9 +176,14 @@ whereClauseExpr
| booleanExpr[ false ]
;
-orderExprs
+orderExprs { String ordExp = null; String ordDir = null; String ordNul = null; }
// TODO: remove goofy space before the comma when we don't have to regression test anymore.
- : ( expr ) (dir:orderDirection { out(" "); out(dir); })? ( {out(", "); } orderExprs)?
+ // Dialect is provided a hook to render each ORDER BY element, so the expression is being captured instead of
+ // printing to the SQL output directly. See Dialect#renderOrderByElement(String, String, String, NullPrecedence).
+ : { captureExpressionStart(); } ( expr ) { captureExpressionFinish(); ordExp = resetCapture(); }
+ (dir:orderDirection { ordDir = #dir.getText(); })? (ordNul=nullOrdering)?
+ { out( renderOrderByElement( ordExp, ordDir, ordNul ) ); }
+ ( {out(", "); } orderExprs )?
;
groupExprs
@@ -167,6 +196,15 @@ orderDirection
| DESCENDING
;
+nullOrdering returns [String nullOrdExp = null]
+ : NULLS fl:nullPrecedence { nullOrdExp = #fl.getText(); }
+ ;
+
+nullPrecedence
+ : FIRST
+ | LAST
+ ;
+
whereExpr
// Expect the filter subtree, followed by the theta join subtree, followed by the HQL condition subtree.
// Might need parens around the HQL condition if there is more than one subtree.
diff --git a/hibernate-core/src/main/java/org/hibernate/NullPrecedence.java b/hibernate-core/src/main/java/org/hibernate/NullPrecedence.java
new file mode 100644
index 0000000000..f4b9dab727
--- /dev/null
+++ b/hibernate-core/src/main/java/org/hibernate/NullPrecedence.java
@@ -0,0 +1,41 @@
+package org.hibernate;
+
+/**
+ * Defines precedence of null values within {@code ORDER BY} clause.
+ *
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
+ */
+public enum NullPrecedence {
+ /**
+ * Null precedence not specified. Relies on the RDBMS implementation.
+ */
+ NONE,
+
+ /**
+ * Null values appear at the beginning of the sorted collection.
+ */
+ FIRST,
+
+ /**
+ * Null values appear at the end of the sorted collection.
+ */
+ LAST;
+
+ public static NullPrecedence parse(String type) {
+ if ( "none".equalsIgnoreCase( type ) ) {
+ return NullPrecedence.NONE;
+ }
+ else if ( "first".equalsIgnoreCase( type ) ) {
+ return NullPrecedence.FIRST;
+ }
+ else if ( "last".equalsIgnoreCase( type ) ) {
+ return NullPrecedence.LAST;
+ }
+ return null;
+ }
+
+ public static NullPrecedence parse(String type, NullPrecedence defaultValue) {
+ final NullPrecedence value = parse( type );
+ return value != null ? value : defaultValue;
+ }
+}
diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java
index b0499923c2..c1a6257f97 100644
--- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java
+++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java
@@ -417,6 +417,12 @@ public interface AvailableSettings {
*/
public static final String ORDER_INSERTS = "hibernate.order_inserts";
+ /**
+ * Default precedence of null values in {@code ORDER BY} clause. Supported options: {@code none} (default),
+ * {@code first}, {@code last}.
+ */
+ public static final String DEFAULT_NULL_ORDERING = "hibernate.order_by.default_null_ordering";
+
/**
* The EntityMode in which set the Session opened from the SessionFactory.
*/
diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java b/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java
index a027f47136..916917ea6a 100644
--- a/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java
+++ b/hibernate-core/src/main/java/org/hibernate/cfg/Settings.java
@@ -28,6 +28,7 @@ import java.util.Map;
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.EntityMode;
import org.hibernate.MultiTenancyStrategy;
+import org.hibernate.NullPrecedence;
import org.hibernate.cache.spi.QueryCacheFactory;
import org.hibernate.cache.spi.RegionFactory;
import org.hibernate.hql.spi.MultiTableBulkIdStrategy;
@@ -83,6 +84,7 @@ public final class Settings {
private boolean namedQueryStartupCheckingEnabled;
private EntityTuplizerFactory entityTuplizerFactory;
private boolean checkNullability;
+ private NullPrecedence defaultNullPrecedence;
private boolean initializeLazyStateOutsideTransactions;
// private ComponentTuplizerFactory componentTuplizerFactory; todo : HHH-3517 and HHH-1907
// private BytecodeProvider bytecodeProvider;
@@ -276,6 +278,9 @@ public final class Settings {
// return componentTuplizerFactory;
// }
+ public NullPrecedence getDefaultNullPrecedence() {
+ return defaultNullPrecedence;
+ }
// package protected setters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -499,4 +504,8 @@ public final class Settings {
public void setDirectReferenceCacheEntriesEnabled(boolean directReferenceCacheEntriesEnabled) {
this.directReferenceCacheEntriesEnabled = directReferenceCacheEntriesEnabled;
}
+
+ void setDefaultNullPrecedence(NullPrecedence defaultNullPrecedence) {
+ this.defaultNullPrecedence = defaultNullPrecedence;
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java b/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java
index ff473d7d50..555682b740 100644
--- a/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java
+++ b/hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java
@@ -27,12 +27,11 @@ import java.io.Serializable;
import java.util.Map;
import java.util.Properties;
-import org.jboss.logging.Logger;
-
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.MultiTenancyStrategy;
+import org.hibernate.NullPrecedence;
import org.hibernate.cache.internal.NoCachingRegionFactory;
import org.hibernate.cache.internal.RegionFactoryInitiator;
import org.hibernate.cache.internal.StandardQueryCacheFactory;
@@ -56,6 +55,7 @@ import org.hibernate.service.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.service.jta.platform.spi.JtaPlatform;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
+import org.jboss.logging.Logger;
/**
* Reads configuration properties and builds a {@link Settings} instance.
@@ -243,6 +243,14 @@ public class SettingsFactory implements Serializable {
}
settings.setOrderInsertsEnabled( orderInserts );
+ String defaultNullPrecedence = ConfigurationHelper.getString(
+ AvailableSettings.DEFAULT_NULL_ORDERING, properties, "none", "first", "last"
+ );
+ if ( debugEnabled ) {
+ LOG.debugf( "Default null ordering: %s", defaultNullPrecedence );
+ }
+ settings.setDefaultNullPrecedence( NullPrecedence.parse( defaultNullPrecedence ) );
+
//Query parser settings:
settings.setQueryTranslatorFactory( createQueryTranslatorFactory( properties, serviceRegistry ) );
diff --git a/hibernate-core/src/main/java/org/hibernate/criterion/Order.java b/hibernate-core/src/main/java/org/hibernate/criterion/Order.java
index a5b623d7c0..68b7b23656 100644
--- a/hibernate-core/src/main/java/org/hibernate/criterion/Order.java
+++ b/hibernate-core/src/main/java/org/hibernate/criterion/Order.java
@@ -28,21 +28,23 @@ import java.sql.Types;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
+import org.hibernate.NullPrecedence;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.type.Type;
/**
* Represents an order imposed upon a Criteria result set
* @author Gavin King
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class Order implements Serializable {
-
private boolean ascending;
private boolean ignoreCase;
private String propertyName;
+ private NullPrecedence nullPrecedence;
public String toString() {
- return propertyName + ' ' + (ascending?"asc":"desc");
+ return propertyName + ' ' + ( ascending ? "asc" : "desc" ) + ( nullPrecedence != null ? ' ' + nullPrecedence.name().toLowerCase() : "" );
}
public Order ignoreCase() {
@@ -50,6 +52,11 @@ public class Order implements Serializable {
return this;
}
+ public Order nulls(NullPrecedence nullPrecedence) {
+ this.nullPrecedence = nullPrecedence;
+ return this;
+ }
+
/**
* Constructor for Order.
*/
@@ -68,15 +75,23 @@ public class Order implements Serializable {
Type type = criteriaQuery.getTypeUsingProjection(criteria, propertyName);
StringBuilder fragment = new StringBuilder();
for ( int i=0; iSELECT clause be wrapped in cast()
* calls to tell the db parser the expected type.
diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java
index ac33edf5b0..e9825d0ca8 100644
--- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java
+++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java
@@ -29,6 +29,7 @@ import java.sql.SQLException;
import java.sql.Types;
import org.hibernate.JDBCException;
+import org.hibernate.NullPrecedence;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.function.NoArgSQLFunction;
import org.hibernate.dialect.function.StandardSQLFunction;
@@ -333,6 +334,24 @@ public class MySQLDialect extends Dialect {
return true;
}
+ @Override
+ public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) {
+ final StringBuilder orderByElement = new StringBuilder();
+ if ( nulls != NullPrecedence.NONE ) {
+ // Workaround for NULLS FIRST / LAST support.
+ orderByElement.append( "case when " ).append( expression ).append( " is null then " );
+ if ( nulls == NullPrecedence.FIRST ) {
+ orderByElement.append( "0 else 1" );
+ }
+ else {
+ orderByElement.append( "1 else 0" );
+ }
+ orderByElement.append( " end, " );
+ }
+ // Nulls precedence has already been handled so passing NONE value.
+ orderByElement.append( super.renderOrderByElement( expression, collation, order, NullPrecedence.NONE ) );
+ return orderByElement.toString();
+ }
// locking support
diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java
index 4973b6b016..0c9b8846b5 100644
--- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java
+++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java
@@ -33,6 +33,7 @@ import antlr.RecognitionException;
import antlr.collections.AST;
import org.jboss.logging.Logger;
+import org.hibernate.NullPrecedence;
import org.hibernate.QueryException;
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.engine.spi.SessionFactoryImplementor;
@@ -366,4 +367,10 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
out( d );
}
}
+
+ @Override
+ protected String renderOrderByElement(String expression, String order, String nulls) {
+ final NullPrecedence nullPrecedence = NullPrecedence.parse( nulls, sessionFactory.getSettings().getDefaultNullPrecedence() );
+ return sessionFactory.getDialect().renderOrderByElement( expression, null, order, nullPrecedence );
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java
index 924961c17e..f17d71c55c 100644
--- a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java
+++ b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java
@@ -25,6 +25,7 @@ package org.hibernate.internal.util.config;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
@@ -81,6 +82,30 @@ public final class ConfigurationHelper {
return value == null ? defaultValue : value;
}
+ /**
+ * Get the config value as a {@link String}.
+ *
+ * @param name The config setting name.
+ * @param values The map of config parameters.
+ * @param defaultValue The default value to use if not found.
+ * @param otherSupportedValues List of other supported values. Does not need to contain the default one.
+ *
+ * @return The value.
+ *
+ * @throws ConfigurationException Unsupported value provided.
+ *
+ */
+ public static String getString(String name, Map values, String defaultValue, String ... otherSupportedValues) {
+ final String value = getString( name, values, defaultValue );
+ if ( !defaultValue.equals( value ) && ArrayHelper.indexOf( otherSupportedValues, value ) == -1 ) {
+ throw new ConfigurationException(
+ "Unsupported configuration [name=" + name + ", value=" + value + "]. " +
+ "Choose value between: '" + defaultValue + "', '" + StringHelper.join( "', '", otherSupportedValues ) + "'."
+ );
+ }
+ return value;
+ }
+
/**
* Get the config value as a boolean (default of false)
*
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentRenderer.java b/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentRenderer.java
index 8544632c46..561b271565 100644
--- a/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentRenderer.java
+++ b/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentRenderer.java
@@ -26,6 +26,8 @@ package org.hibernate.sql.ordering.antlr;
import antlr.collections.AST;
import org.jboss.logging.Logger;
+import org.hibernate.NullPrecedence;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.internal.util.StringHelper;
@@ -41,6 +43,12 @@ public class OrderByFragmentRenderer extends GeneratedOrderByFragmentRenderer {
private static final Logger LOG = Logger.getLogger( OrderByFragmentRenderer.class.getName() );
private static final ASTPrinter printer = new ASTPrinter( GeneratedOrderByFragmentRendererTokenTypes.class );
+ private final SessionFactoryImplementor sessionFactory;
+
+ public OrderByFragmentRenderer(SessionFactoryImplementor sessionFactory) {
+ this.sessionFactory = sessionFactory;
+ }
+
@Override
protected void out(AST ast) {
out( ( ( Node ) ast ).getRenderableText() );
@@ -75,4 +83,10 @@ public class OrderByFragmentRenderer extends GeneratedOrderByFragmentRenderer {
String prefix = "<-" + StringHelper.repeat( '-', (--traceDepth * 2) ) + " ";
LOG.trace( prefix + ruleName );
}
+
+ @Override
+ protected String renderOrderByElement(String expression, String collation, String order, String nulls) {
+ final NullPrecedence nullPrecedence = NullPrecedence.parse( nulls, sessionFactory.getSettings().getDefaultNullPrecedence() );
+ return sessionFactory.getDialect().renderOrderByElement( expression, collation, order, nullPrecedence );
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentTranslator.java
index ec88066bd1..5e5be00003 100644
--- a/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentTranslator.java
+++ b/hibernate-core/src/main/java/org/hibernate/sql/ordering/antlr/OrderByFragmentTranslator.java
@@ -75,7 +75,7 @@ public class OrderByFragmentTranslator {
}
// Render the parsed tree to text.
- OrderByFragmentRenderer renderer = new OrderByFragmentRenderer();
+ OrderByFragmentRenderer renderer = new OrderByFragmentRenderer( context.getSessionFactory() );
try {
renderer.orderByFragment( parser.getAST() );
}
diff --git a/hibernate-core/src/test/java/org/hibernate/sql/TemplateTest.java b/hibernate-core/src/test/java/org/hibernate/sql/TemplateTest.java
index 80a69ce7e3..58e99494b1 100644
--- a/hibernate-core/src/test/java/org/hibernate/sql/TemplateTest.java
+++ b/hibernate-core/src/test/java/org/hibernate/sql/TemplateTest.java
@@ -25,18 +25,24 @@ package org.hibernate.sql;
import java.util.Collections;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.hibernate.QueryException;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.cfg.Configuration;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.dialect.function.SQLFunctionRegistry;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.persister.entity.PropertyMapping;
+import org.hibernate.service.ServiceRegistry;
import org.hibernate.sql.ordering.antlr.ColumnMapper;
-import org.hibernate.testing.junit4.BaseUnitTestCase;
-
import org.hibernate.sql.ordering.antlr.ColumnReference;
import org.hibernate.sql.ordering.antlr.SqlValueReference;
+import org.hibernate.testing.ServiceRegistryBuilder;
+import org.hibernate.testing.junit4.BaseUnitTestCase;
import org.hibernate.type.Type;
import static org.junit.Assert.assertEquals;
@@ -100,6 +106,23 @@ public class TemplateTest extends BaseUnitTestCase {
private static final SQLFunctionRegistry FUNCTION_REGISTRY = new SQLFunctionRegistry( DIALECT, Collections.EMPTY_MAP );
+ private static SessionFactoryImplementor SESSION_FACTORY = null; // Required for ORDER BY rendering.
+
+ @BeforeClass
+ public static void buildSessionFactory() {
+ Configuration cfg = new Configuration();
+ cfg.setProperty( AvailableSettings.DIALECT, DIALECT.getClass().getName() );
+ ServiceRegistry serviceRegistry = ServiceRegistryBuilder.buildServiceRegistry( cfg.getProperties() );
+ SESSION_FACTORY = (SessionFactoryImplementor) cfg.buildSessionFactory( serviceRegistry );
+ }
+
+ @AfterClass
+ public static void closeSessionFactory() {
+ if ( SESSION_FACTORY != null ) {
+ SESSION_FACTORY.close();
+ }
+ }
+
@Test
public void testSqlExtractFunction() {
String fragment = "extract( year from col )";
@@ -244,6 +267,6 @@ public class TemplateTest extends BaseUnitTestCase {
}
public String doStandardRendering(String fragment) {
- return Template.renderOrderByStringTemplate( fragment, MAPPER, null, DIALECT, FUNCTION_REGISTRY );
+ return Template.renderOrderByStringTemplate( fragment, MAPPER, SESSION_FACTORY, DIALECT, FUNCTION_REGISTRY );
}
}
\ No newline at end of file
diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/DefaultNullOrderingTest.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/DefaultNullOrderingTest.java
new file mode 100644
index 0000000000..40842e6b61
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/DefaultNullOrderingTest.java
@@ -0,0 +1,132 @@
+package org.hibernate.test.annotations.onetomany;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.hibernate.Criteria;
+import org.hibernate.Session;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.dialect.H2Dialect;
+import org.hibernate.testing.RequiresDialect;
+import org.hibernate.testing.TestForIssue;
+import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
+
+/**
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
+ */
+@TestForIssue(jiraKey = "HHH-465")
+@RequiresDialect(value = H2Dialect.class,
+ comment = "By default H2 places NULL values first, so testing 'NULLS LAST' expression.")
+public class DefaultNullOrderingTest extends BaseCoreFunctionalTestCase {
+ @Override
+ protected void configure(Configuration configuration) {
+ configuration.setProperty( AvailableSettings.DEFAULT_NULL_ORDERING, "last" );
+ }
+
+ @Override
+ protected Class[] getAnnotatedClasses() {
+ return new Class[] { Monkey.class, Troop.class, Soldier.class };
+ }
+
+ @Test
+ public void testHqlDefaultNullOrdering() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Monkey monkey1 = new Monkey();
+ monkey1.setName( null );
+ Monkey monkey2 = new Monkey();
+ monkey2.setName( "Warsaw ZOO" );
+ session.persist( monkey1 );
+ session.persist( monkey2 );
+ session.getTransaction().commit();
+
+ session.getTransaction().begin();
+ List orderedResults = (List) session.createQuery( "from Monkey m order by m.name" ).list(); // Should order by NULLS LAST.
+ Assert.assertEquals( Arrays.asList( monkey2, monkey1 ), orderedResults );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( monkey1 );
+ session.delete( monkey2 );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
+ @Test
+ public void testAnnotationsDefaultNullOrdering() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Troop troop = new Troop();
+ troop.setName( "Alpha 1" );
+ Soldier ranger = new Soldier();
+ ranger.setName( "Ranger 1" );
+ troop.addSoldier( ranger );
+ Soldier sniper = new Soldier();
+ sniper.setName( null );
+ troop.addSoldier( sniper );
+ session.persist( troop );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ session.getTransaction().begin();
+ troop = (Troop) session.get( Troop.class, troop.getId() );
+ Iterator iterator = troop.getSoldiers().iterator(); // Should order by NULLS LAST.
+ Assert.assertEquals( ranger.getName(), iterator.next().getName() );
+ Assert.assertNull( iterator.next().getName() );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( troop );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
+ @Test
+ public void testCriteriaDefaultNullOrdering() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Monkey monkey1 = new Monkey();
+ monkey1.setName( null );
+ Monkey monkey2 = new Monkey();
+ monkey2.setName( "Berlin ZOO" );
+ session.persist( monkey1 );
+ session.persist( monkey2 );
+ session.getTransaction().commit();
+
+ session.getTransaction().begin();
+ Criteria criteria = session.createCriteria( Monkey.class );
+ criteria.addOrder( org.hibernate.criterion.Order.asc( "name" ) ); // Should order by NULLS LAST.
+ Assert.assertEquals( Arrays.asList( monkey2, monkey1 ), criteria.list() );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( monkey1 );
+ session.delete( monkey2 );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OrderByTest.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OrderByTest.java
index 314c7588de..3f923434fc 100644
--- a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OrderByTest.java
+++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/OrderByTest.java
@@ -23,15 +23,27 @@
*/
package org.hibernate.test.annotations.onetomany;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
import org.junit.Test;
+import org.hibernate.Criteria;
+import org.hibernate.NullPrecedence;
import org.hibernate.Session;
+import org.hibernate.dialect.H2Dialect;
+import org.hibernate.dialect.MySQLDialect;
+import org.hibernate.testing.RequiresDialect;
+import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
/**
* @author Emmanuel Bernard
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class OrderByTest extends BaseCoreFunctionalTestCase {
@Test
@@ -70,8 +82,189 @@ public class OrderByTest extends BaseCoreFunctionalTestCase {
s.close();
}
+ @Test
+ @TestForIssue(jiraKey = "HHH-465")
+ @RequiresDialect(value = { H2Dialect.class, MySQLDialect.class },
+ comment = "By default H2 places NULL values first, so testing 'NULLS LAST' expression. " +
+ "For MySQL testing overridden Dialect#renderOrderByElement(String, String, String, NullPrecedence) method. " +
+ "MySQL does not support NULLS FIRST / LAST syntax at the moment, so transforming the expression to 'CASE WHEN ...'.")
+ public void testAnnotationNullsFirstLast() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Tiger tiger1 = new Tiger();
+ tiger1.setName( null ); // Explicitly setting null value.
+ Tiger tiger2 = new Tiger();
+ tiger2.setName( "Max" );
+ Monkey monkey1 = new Monkey();
+ monkey1.setName( "Michael" );
+ Monkey monkey2 = new Monkey();
+ monkey2.setName( null ); // Explicitly setting null value.
+ Zoo zoo = new Zoo( "Warsaw ZOO" );
+ zoo.getTigers().add( tiger1 );
+ zoo.getTigers().add( tiger2 );
+ zoo.getMonkeys().add( monkey1 );
+ zoo.getMonkeys().add( monkey2 );
+ session.persist( zoo );
+ session.persist( tiger1 );
+ session.persist( tiger2 );
+ session.persist( monkey1 );
+ session.persist( monkey2 );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ session.getTransaction().begin();
+ zoo = (Zoo) session.get( Zoo.class, zoo.getId() );
+ // Testing @org.hibernate.annotations.OrderBy.
+ Iterator iterator1 = zoo.getTigers().iterator();
+ Assert.assertEquals( tiger2.getName(), iterator1.next().getName() );
+ Assert.assertNull( iterator1.next().getName() );
+ // Testing @javax.persistence.OrderBy.
+ Iterator iterator2 = zoo.getMonkeys().iterator();
+ Assert.assertEquals( monkey1.getName(), iterator2.next().getName() );
+ Assert.assertNull( iterator2.next().getName() );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( tiger1 );
+ session.delete( tiger2 );
+ session.delete( monkey1 );
+ session.delete( monkey2 );
+ session.delete( zoo );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
+ @Test
+ @TestForIssue(jiraKey = "HHH-465")
+ @RequiresDialect(value = { H2Dialect.class, MySQLDialect.class },
+ comment = "By default H2 places NULL values first, so testing 'NULLS LAST' expression. " +
+ "For MySQL testing overridden Dialect#renderOrderByElement(String, String, String, NullPrecedence) method. " +
+ "MySQL does not support NULLS FIRST / LAST syntax at the moment, so transforming the expression to 'CASE WHEN ...'.")
+ public void testCriteriaNullsFirstLast() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Zoo zoo1 = new Zoo( null );
+ Zoo zoo2 = new Zoo( "Warsaw ZOO" );
+ session.persist( zoo1 );
+ session.persist( zoo2 );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ session.getTransaction().begin();
+ Criteria criteria = session.createCriteria( Zoo.class );
+ criteria.addOrder( org.hibernate.criterion.Order.asc( "name" ).nulls( NullPrecedence.LAST ) );
+ Iterator iterator = (Iterator) criteria.list().iterator();
+ Assert.assertEquals( zoo2.getName(), iterator.next().getName() );
+ Assert.assertNull( iterator.next().getName() );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( zoo1 );
+ session.delete( zoo2 );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
+ @Test
+ @TestForIssue(jiraKey = "HHH-465")
+ @RequiresDialect(value = { H2Dialect.class, MySQLDialect.class },
+ comment = "By default H2 places NULL values first, so testing 'NULLS LAST' expression. " +
+ "For MySQL testing overridden Dialect#renderOrderByElement(String, String, String, NullPrecedence) method. " +
+ "MySQL does not support NULLS FIRST / LAST syntax at the moment, so transforming the expression to 'CASE WHEN ...'.")
+ public void testNullsFirstLastSpawnMultipleColumns() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Zoo zoo = new Zoo();
+ zoo.setName( "Berlin ZOO" );
+ Visitor visitor1 = new Visitor( null, null );
+ Visitor visitor2 = new Visitor( null, "Antoniak" );
+ Visitor visitor3 = new Visitor( "Lukasz", "Antoniak" );
+ zoo.getVisitors().add( visitor1 );
+ zoo.getVisitors().add( visitor2 );
+ zoo.getVisitors().add( visitor3 );
+ session.save( zoo );
+ session.save( visitor1 );
+ session.save( visitor2 );
+ session.save( visitor3 );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ session.getTransaction().begin();
+ zoo = (Zoo) session.get( Zoo.class, zoo.getId() );
+ Iterator iterator = zoo.getVisitors().iterator();
+ Assert.assertEquals( 3, zoo.getVisitors().size() );
+ Assert.assertEquals( visitor3, iterator.next() );
+ Assert.assertEquals( visitor2, iterator.next() );
+ Assert.assertEquals( visitor1, iterator.next() );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( visitor1 );
+ session.delete( visitor2 );
+ session.delete( visitor3 );
+ session.delete( zoo );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
+ @Test
+ @TestForIssue(jiraKey = "HHH-465")
+ @RequiresDialect(value = { H2Dialect.class, MySQLDialect.class },
+ comment = "By default H2 places NULL values first, so testing 'NULLS LAST' expression. " +
+ "For MySQL testing overridden Dialect#renderOrderByElement(String, String, String, NullPrecedence) method. " +
+ "MySQL does not support NULLS FIRST / LAST syntax at the moment, so transforming the expression to 'CASE WHEN ...'.")
+ public void testHqlNullsFirstLast() {
+ Session session = openSession();
+
+ // Populating database with test data.
+ session.getTransaction().begin();
+ Zoo zoo1 = new Zoo();
+ zoo1.setName( null );
+ Zoo zoo2 = new Zoo();
+ zoo2.setName( "Warsaw ZOO" );
+ session.persist( zoo1 );
+ session.persist( zoo2 );
+ session.getTransaction().commit();
+
+ session.getTransaction().begin();
+ List orderedResults = (List) session.createQuery( "from Zoo z order by z.name nulls lAsT" ).list();
+ Assert.assertEquals( Arrays.asList( zoo2, zoo1 ), orderedResults );
+ session.getTransaction().commit();
+
+ session.clear();
+
+ // Cleanup data.
+ session.getTransaction().begin();
+ session.delete( zoo1 );
+ session.delete( zoo2 );
+ session.getTransaction().commit();
+
+ session.close();
+ }
+
@Override
protected Class[] getAnnotatedClasses() {
- return new Class[] { Order.class, OrderItem.class };
+ return new Class[] { Order.class, OrderItem.class, Zoo.class, Tiger.class, Monkey.class, Visitor.class };
}
}
diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Soldier.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Soldier.java
index b0ac65a594..6df82f1b63 100644
--- a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Soldier.java
+++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Soldier.java
@@ -44,18 +44,20 @@ public class Soldier {
this.troop = troop;
}
+ @Override
public boolean equals(Object o) {
if ( this == o ) return true;
if ( !( o instanceof Soldier ) ) return false;
final Soldier soldier = (Soldier) o;
- if ( !name.equals( soldier.name ) ) return false;
+ if ( name != null ? !name.equals( soldier.name ) : soldier.name != null ) return false;
return true;
}
+ @Override
public int hashCode() {
- return name.hashCode();
+ return name != null ? name.hashCode() : 0;
}
}
diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Visitor.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Visitor.java
new file mode 100644
index 0000000000..2dfc0030ee
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Visitor.java
@@ -0,0 +1,79 @@
+package org.hibernate.test.annotations.onetomany;
+
+import java.io.Serializable;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+/**
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
+ */
+@Entity
+public class Visitor implements Serializable {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ private String firstName;
+
+ private String lastName;
+
+ public Visitor() {
+ }
+
+ public Visitor(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) return true;
+ if ( ! ( o instanceof Visitor) ) return false;
+
+ Visitor visitor = (Visitor) o;
+
+ if ( firstName != null ? !firstName.equals( visitor.firstName ) : visitor.firstName != null ) return false;
+ if ( id != null ? !id.equals( visitor.id ) : visitor.id != null ) return false;
+ if ( lastName != null ? !lastName.equals( visitor.lastName ) : visitor.lastName != null ) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + ( firstName != null ? firstName.hashCode() : 0 );
+ result = 31 * result + ( lastName != null ? lastName.hashCode() : 0 );
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Visitor(id = " + id + ", firstName = " + firstName + ", lastName = " + lastName + ")";
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Zoo.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Zoo.java
new file mode 100644
index 0000000000..0609e06890
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/onetomany/Zoo.java
@@ -0,0 +1,111 @@
+package org.hibernate.test.annotations.onetomany;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+
+/**
+ * Entity used to test {@code NULL} values ordering in SQL {@code ORDER BY} clause.
+ * Implementation note: By default H2 places {@code NULL} values first.
+ * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
+ */
+@Entity
+public class Zoo implements Serializable {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ private String name;
+
+ @OneToMany
+ @JoinColumn(name = "zoo_id")
+ @org.hibernate.annotations.OrderBy(clause = "name asc nulls last") // By default H2 places NULL values first.
+ private Set tigers = new HashSet();
+
+ @OneToMany
+ @JoinColumn(name = "zoo_id")
+ @javax.persistence.OrderBy("name asc nulls last") // According to JPA specification this is illegal, but works in Hibernate.
+ private Set monkeys = new HashSet();
+
+ @OneToMany
+ @JoinColumn(name = "zoo_id")
+ @javax.persistence.OrderBy("lastName desc nulls last, firstName asc nulls LaSt") // Sorting by multiple columns.
+ private Set visitors = new HashSet();
+
+ public Zoo() {
+ }
+
+ public Zoo(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) return true;
+ if ( ! ( o instanceof Zoo ) ) return false;
+
+ Zoo zoo = (Zoo) o;
+
+ if ( id != null ? !id.equals( zoo.id ) : zoo.id != null ) return false;
+ if ( name != null ? !name.equals( zoo.name ) : zoo.name != null ) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + ( name != null ? name.hashCode() : 0 );
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Zoo(id = " + id + ", name = " + name + ")";
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Set getTigers() {
+ return tigers;
+ }
+
+ public void setTigers(Set tigers) {
+ this.tigers = tigers;
+ }
+
+ public Set getMonkeys() {
+ return monkeys;
+ }
+
+ public void setMonkeys(Set monkeys) {
+ this.monkeys = monkeys;
+ }
+
+ public Set getVisitors() {
+ return visitors;
+ }
+
+ public void setVisitors(Set visitors) {
+ this.visitors = visitors;
+ }
+}