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:
parent
3e956a7a58
commit
5bf8d84379
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
;
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
*
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + ")";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue