HHH-465 - Support for NULLS FIRST/LAST

Conflicts:
	hibernate-core/src/main/java/org/hibernate/cfg/Settings.java
	hibernate-core/src/main/java/org/hibernate/criterion/Order.java

Conflicts:
	hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java
	hibernate-core/src/main/java/org/hibernate/criterion/Order.java
This commit is contained in:
Lukasz Antoniak 2013-01-28 14:19:58 -05:00 committed by Brett Meyer
parent 3e956a7a58
commit 5bf8d84379
27 changed files with 869 additions and 46 deletions

View File

@ -119,7 +119,7 @@ List cats = sess.createCriteria(Cat.class)
<programlisting role="JAVA"><![CDATA[List cats = sess.createCriteria(Cat.class)
.add( Restrictions.like("name", "F%")
.addOrder( Order.asc("name") )
.addOrder( Order.asc("name").nulls(NullPrecedence.LAST) )
.addOrder( Order.desc("age") )
.setMaxResults(50)
.list();]]></programlisting>

View File

@ -77,6 +77,12 @@
<entry>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.</entry>
</row>
<row>
<entry>hibernate.order_by.default_null_ordering</entry>
<entry><para><literal>none</literal>, <literal>first</literal> or <literal>last</literal></para></entry>
<entry>Defines precedence of null values in <literal>ORDER BY</literal> clause. Defaults to
<literal>none</literal> which varies between RDBMS implementation.</entry>
</row>
<row>
<entry>hibernate.generate_statistics</entry>
<entry><para><literal>true</literal> or <literal>false</literal></para></entry>

View File

@ -1427,7 +1427,9 @@
</para>
<para>
Individual expressions in the order-by can be qualified with either <literal>ASC</literal> (ascending) or
<literal>DESC</literal> (descending) to indicated the desired ordering direction.
<literal>DESC</literal> (descending) to indicated the desired ordering direction. Null values can be placed
in front or at the end of sorted set using <literal>NULLS FIRST</literal> or <literal>NULLS LAST</literal>
clause respectively.
</para>
<example>
<title>Order-by examples</title>

View File

@ -483,8 +483,8 @@ public class Part {
<literal>@javax.persistence.OrderBy</literal> to your property. This
annotation takes as parameter a list of comma separated properties (of
the target entity) and orders the collection accordingly (eg
<code>firstname asc, age desc</code>), if the string is empty, the
collection will be ordered by the primary key of the target
<code>firstname asc, age desc, weight asc nulls last</code>), if the string
is empty, the collection will be ordered by the primary key of the target
entity.</para>
<example>

View File

@ -855,12 +855,17 @@ WHERE prod.name = 'widget'
</para>
<programlisting><![CDATA[from DomesticCat cat
order by cat.name asc, cat.weight desc, cat.birthdate]]></programlisting>
order by cat.name asc, cat.weight desc nulls first, cat.birthdate]]></programlisting>
<para>
The optional <literal>asc</literal> or <literal>desc</literal> indicate ascending or descending order
respectively.
</para>
<para>
The optional <literal>nulls first</literal> or <literal>nulls last</literal> indicate precedence of null
values while sorting.
</para>
</section>
<section xml:id="queryhql-grouping" revision="1">

View File

@ -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

View File

@ -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;

View File

@ -30,6 +30,7 @@ package org.hibernate.sql.ordering.antlr;
* Antlr grammar for rendering <tt>ORDER_BY</tt> 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(); }
;

View File

@ -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 <tt>null ordering</tt>; <tt>NULLS FIRST</tt> or
* <tt>NULLS LAST</tt>.
*/
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
*/

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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.
*/

View File

@ -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;
}
}

View File

@ -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 ) );

View File

@ -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 <tt>Criteria</tt> 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; i<columns.length; i++ ) {
final StringBuilder expression = new StringBuilder();
SessionFactoryImplementor factory = criteriaQuery.getFactory();
boolean lower = ignoreCase && type.sqlTypes( factory )[i]==Types.VARCHAR;
if (lower) {
fragment.append( factory.getDialect().getLowercaseFunction() )
.append('(');
expression.append( factory.getDialect().getLowercaseFunction() ).append('(');
}
fragment.append( columns[i] );
if (lower) fragment.append(')');
fragment.append( ascending ? " asc" : " desc" );
expression.append( columns[i] );
if (lower) expression.append(')');
fragment.append(
factory.getDialect()
.renderOrderByElement(
expression.toString(),
null,
ascending ? "asc" : "desc",
nullPrecedence != null ? nullPrecedence : factory.getSettings().getDefaultNullPrecedence()
)
);
if ( i<columns.length-1 ) fragment.append(", ");
}
return fragment.toString();

View File

@ -43,6 +43,7 @@ import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.NullPrecedence;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.function.CastFunction;
import org.hibernate.dialect.function.SQLFunction;
@ -2074,6 +2075,30 @@ public abstract class Dialect implements ConversionContext {
return false;
}
/**
* @param expression The SQL order expression. In case of {@code @OrderBy} annotation user receives property placeholder
* (e.g. attribute name enclosed in '{' and '}' signs).
* @param collation Collation string in format {@code collate IDENTIFIER}, or {@code null}
* if expression has not been explicitly specified.
* @param order Order direction. Possible values: {@code asc}, {@code desc}, or {@code null}
* if expression has not been explicitly specified.
* @param nulls Nulls precedence. Default value: {@link NullPrecedence#NONE}.
* @return Renders single element of {@code ORDER BY} clause.
*/
public String renderOrderByElement(String expression, String collation, String order, NullPrecedence nulls) {
final StringBuilder orderByElement = new StringBuilder( expression );
if ( collation != null ) {
orderByElement.append( " " ).append( collation );
}
if ( order != null ) {
orderByElement.append( " " ).append( order );
}
if ( nulls != NullPrecedence.NONE ) {
orderByElement.append( " nulls " ).append( nulls.name().toLowerCase() );
}
return orderByElement.toString();
}
/**
* Does this dialect require that parameters appearing in the <tt>SELECT</tt> clause be wrapped in <tt>cast()</tt>
* calls to tell the db parser the expected type.

View File

@ -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

View File

@ -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 );
}
}

View File

@ -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)
*

View File

@ -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 );
}
}

View File

@ -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() );
}

View File

@ -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 );
}
}

View File

@ -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<Zoo> orderedResults = (List<Zoo>) 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<Soldier> 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();
}
}

View File

@ -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<Tiger> iterator1 = zoo.getTigers().iterator();
Assert.assertEquals( tiger2.getName(), iterator1.next().getName() );
Assert.assertNull( iterator1.next().getName() );
// Testing @javax.persistence.OrderBy.
Iterator<Monkey> 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<Zoo> iterator = (Iterator<Zoo>) 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<Visitor> 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<Zoo> orderedResults = (List<Zoo>) 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 };
}
}

View File

@ -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;
}
}

View File

@ -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 + ")";
}
}

View File

@ -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<Tiger> tigers = new HashSet<Tiger>();
@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<Monkey> monkeys = new HashSet<Monkey>();
@OneToMany
@JoinColumn(name = "zoo_id")
@javax.persistence.OrderBy("lastName desc nulls last, firstName asc nulls LaSt") // Sorting by multiple columns.
private Set<Visitor> visitors = new HashSet<Visitor>();
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<Tiger> getTigers() {
return tigers;
}
public void setTigers(Set<Tiger> tigers) {
this.tigers = tigers;
}
public Set<Monkey> getMonkeys() {
return monkeys;
}
public void setMonkeys(Set<Monkey> monkeys) {
this.monkeys = monkeys;
}
public Set<Visitor> getVisitors() {
return visitors;
}
public void setVisitors(Set<Visitor> visitors) {
this.visitors = visitors;
}
}