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