diff --git a/.gitignore b/.gitignore index 8b8ed3008f..4b79d87c01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # Typically *NIX text editors, by default, append '~' to files on saving to make backups *~ -# Gradle work directory +# Gradle work directory and caches .gradle +.gradletasknamecache # Build output directies /target diff --git a/.gradletasknamecache b/.gradletasknamecache deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/documentation/src/main/asciidoc/userguide/appendices/Configurations.adoc b/documentation/src/main/asciidoc/userguide/appendices/Configurations.adoc index 94ab56c88e..94d3833217 100644 --- a/documentation/src/main/asciidoc/userguide/appendices/Configurations.adoc +++ b/documentation/src/main/asciidoc/userguide/appendices/Configurations.adoc @@ -275,6 +275,14 @@ Please report your mapping that causes the problem to us so we can examine the d + The default value is `false` which means Hibernate will use an algorithm to determine if the insert can be delayed or if the insert should be performed immediately. +`*hibernate.id.sequence.increment_size_mismatch_strategy*` (e.g. `LOG`, `FIX` or `EXCEPTION` (default value)):: +This setting defines the `org.hibernate.id.SequenceMismatchStrategy` used when +Hibernate detects a mismatch between a sequence configuration in an entity mapping +and its database sequence object counterpart. ++ +The default value is given by the `org.hibernate.id.SequenceMismatchStrategy#EXCEPTION`, +meaning that an Exception is thrown when detecting such a conflict. + ==== Quoting options `*hibernate.globally_quoted_identifiers*` (e.g. `true` or `false` (default value)):: @@ -511,6 +519,30 @@ Raises an exception when in-memory pagination over collection fetch is about to + Disabled by default. Set to true to enable. +`*hibernate.query.immutable_entity_update_query_handling_mode*` (e.g. `EXCEPTION` or `WARNING` (default value)):: +Defines how `Immutable` entities are handled when executing a bulk update query. ++ +By default, the (`ImmutableEntityUpdateQueryHandlingMode#WARNING`) mode is used, meaning that +a warning log message is issued when an `@Immutable` entity is to be updated via a bulk update statement. ++ +If the (`ImmutableEntityUpdateQueryHandlingMode#EXCEPTION`) mode is used, then a `HibernateException` is thrown instead. + +`*hibernate.query.in_clause_parameter_padding*` (e.g. `true` or `false` (default value)):: +By default, the IN clause expands to include all bind parameter values. ++ +However, for database systems supporting execution plan caching, +there's a better chance of hitting the cache if the number of possible IN clause parameters lowers. ++ +For this reason, we can expand the bind parameters to power-of-two: 4, 8, 16, 32, 64. +This way, an IN clause with 5, 6, or 7 bind parameters will use the 8 IN clause, +therefore reusing its execution plan. + +`*hibernate.query.omit_join_of_superclass_tables*` (e.g. `false` or `true` (default value)):: +When you use `javax.persistence.InheritanceType#JOINED` strategy for inheritance mapping and query +a value from an entity, all superclass tables are joined in the query regardless you need them. ++ +With this setting set to true only superclass tables which are really needed are joined. + ==== Multi-table bulk HQL operations `*hibernate.hql.bulk_id_strategy*` (e.g. A fully-qualified class name, an instance, or a `Class` object reference):: diff --git a/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java b/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java index 326c4715af..fb1d4f7ec1 100644 --- a/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java @@ -15,10 +15,12 @@ * processed by auto-flush based on the table to which those entities are mapped and which are * determined to have pending state changes. * - * In a similar manner, these query spaces also affect how query result caching can recognize invalidated results. + * In a similar manner, these query spaces also affect how query result caching can recognize + * invalidated results. * * @author Steve Ebersole */ +@SuppressWarnings( { "unused", "UnusedReturnValue", "RedundantSuppression" } ) public interface SynchronizeableQuery { /** * Obtain the list of query spaces the query is synchronized on. @@ -36,6 +38,18 @@ public interface SynchronizeableQuery { */ SynchronizeableQuery addSynchronizedQuerySpace(String querySpace); + /** + * Adds one-or-more synchronized spaces + */ + default SynchronizeableQuery addSynchronizedQuerySpace(String... querySpaces) { + if ( querySpaces != null ) { + for ( int i = 0; i < querySpaces.length; i++ ) { + addSynchronizedQuerySpace( querySpaces[i] ); + } + } + return this; + } + /** * Adds a table expression as a query space. */ @@ -43,6 +57,13 @@ default SynchronizeableQuery addSynchronizedTable(String tableExpression) { return addSynchronizedQuerySpace( tableExpression ); } + /** + * Adds one-or-more synchronized table expressions + */ + default SynchronizeableQuery addSynchronizedTable(String... tableExpressions) { + return addSynchronizedQuerySpace( tableExpressions ); + } + /** * Adds an entity name for (a) auto-flush checking and (b) query result cache invalidation checking. Same as * {@link #addSynchronizedQuerySpace} for all tables associated with the given entity. @@ -55,6 +76,18 @@ default SynchronizeableQuery addSynchronizedTable(String tableExpression) { */ SynchronizeableQuery addSynchronizedEntityName(String entityName) throws MappingException; + /** + * Adds one-or-more entities (by name) whose tables should be added as synchronized spaces + */ + default SynchronizeableQuery addSynchronizedEntityName(String... entityNames) throws MappingException { + if ( entityNames != null ) { + for ( int i = 0; i < entityNames.length; i++ ) { + addSynchronizedEntityName( entityNames[i] ); + } + } + return this; + } + /** * Adds an entity for (a) auto-flush checking and (b) query result cache invalidation checking. Same as * {@link #addSynchronizedQuerySpace} for all tables associated with the given entity. @@ -65,5 +98,18 @@ default SynchronizeableQuery addSynchronizedTable(String tableExpression) { * * @throws MappingException Indicates the given class could not be resolved as an entity */ + @SuppressWarnings( "rawtypes" ) SynchronizeableQuery addSynchronizedEntityClass(Class entityClass) throws MappingException; + + /** + * Adds one-or-more entities (by class) whose tables should be added as synchronized spaces + */ + default SynchronizeableQuery addSynchronizedEntityClass(Class... entityClasses) throws MappingException { + if ( entityClasses != null ) { + for ( int i = 0; i < entityClasses.length; i++ ) { + addSynchronizedEntityClass( entityClasses[i] ); + } + } + return this; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java b/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java index 9d891a833c..f019df9a9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java @@ -89,4 +89,11 @@ * Whether the results should be read-only. Default is {@code false}. */ boolean readOnly() default false; + + /** + * The query spaces to apply for the query. + * + * @see org.hibernate.SynchronizeableQuery + */ + String[] querySpaces() default {}; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java b/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java index c84e41ce33..c005ca2d4c 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java @@ -143,4 +143,17 @@ private QueryHints() { */ public static final String PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough"; + /** + * Hint for specifying query spaces to be applied to a native (SQL) query. + * + * Passed value can be any of:
    + *
  • List of the spaces
  • + *
  • array of the spaces
  • + *
  • String "whitespace"-separated list of the spaces
  • + *
+ * + * @see org.hibernate.SynchronizeableQuery + */ + public static final String NATIVE_SPACES = "org.hibernate.query.native.spaces"; + } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/AbstractPluralAttributeSourceImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/AbstractPluralAttributeSourceImpl.java index 88905125d6..a3463d12e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/AbstractPluralAttributeSourceImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/AbstractPluralAttributeSourceImpl.java @@ -6,10 +6,14 @@ */ package org.hibernate.boot.model.source.internal.hbm; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.hibernate.AssertionFailure; import org.hibernate.boot.MappingException; +import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmColumnType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmFilterType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmManyToOneType; import org.hibernate.boot.jaxb.hbm.spi.JaxbHbmRootEntityType; @@ -72,17 +76,58 @@ protected AbstractPluralAttributeSourceImpl( Optional jaxbHbmManyToOneTypeOptional = Optional.empty(); - if ( pluralAttributeJaxbMapping.isInverse() && pluralAttributeJaxbMapping.getOneToMany() != null ) { + // Our goal here is to find the inverse side of a one to many to figure out against what to join + if ( pluralAttributeJaxbMapping.isInverse() && pluralAttributeJaxbMapping.getOneToMany() != null && pluralAttributeJaxbMapping.getKey().getPropertyRef() == null ) { String childClass = pluralAttributeJaxbMapping.getOneToMany().getClazz(); if ( childClass != null ) { + // We match by columns as defined in the key + final List keyColumnNames; + if ( pluralAttributeJaxbMapping.getKey().getColumnAttribute() == null ) { + keyColumnNames = new ArrayList<>( pluralAttributeJaxbMapping.getKey().getColumn().size() ); + for ( JaxbHbmColumnType jaxbHbmColumnType : pluralAttributeJaxbMapping.getKey().getColumn() ) { + keyColumnNames.add( jaxbHbmColumnType.getName() ); + } + } + else { + keyColumnNames = new ArrayList<>( 1 ); + keyColumnNames.add( pluralAttributeJaxbMapping.getKey().getColumnAttribute() ); + } jaxbHbmManyToOneTypeOptional = mappingDocument.getDocumentRoot().getClazz() .stream() .filter( (JaxbHbmRootEntityType entityType) -> childClass.equals( entityType.getName() ) ) .flatMap( jaxbHbmRootEntityType -> jaxbHbmRootEntityType.getAttributes().stream() ) - .filter( - attribute -> attribute instanceof JaxbHbmManyToOneType && - ( (JaxbHbmManyToOneType) attribute ).getPropertyRef() != null ) + .filter( attribute -> { + if ( attribute instanceof JaxbHbmManyToOneType ) { + JaxbHbmManyToOneType manyToOneType = (JaxbHbmManyToOneType) attribute; + String manyToOneTypeClass = manyToOneType.getClazz(); + String containerClass = container.getAttributeRoleBase().getFullPath(); + // Consider many to ones that have no class defined or equal the owner class of the one to many + if ( manyToOneTypeClass == null || manyToOneTypeClass.equals( containerClass ) ) { + if ( manyToOneType.getColumnAttribute() == null ) { + List columns = manyToOneType.getColumnOrFormula(); + if ( columns.size() != keyColumnNames.size() ) { + return false; + } + for ( int i = 0; i < columns.size(); i++ ) { + Serializable column = columns.get( i ); + String keyColumn = keyColumnNames.get( i ); + if ( !( column instanceof JaxbHbmColumnType ) || !( (JaxbHbmColumnType) column ) + .getName() + .equals( keyColumn ) ) { + return false; + } + } + } + else { + return keyColumnNames.size() == 1 && keyColumnNames.get( 0 ) + .equals( manyToOneType.getColumnAttribute() ); + } + return true; + } + } + return false; + }) .map( JaxbHbmManyToOneType.class::cast ) .findFirst(); } @@ -91,13 +136,12 @@ protected AbstractPluralAttributeSourceImpl( this.keySource = jaxbHbmManyToOneTypeOptional .map( jaxbHbmManyToOneType -> new PluralAttributeKeySourceImpl( sourceMappingDocument(), + pluralAttributeJaxbMapping.getKey(), jaxbHbmManyToOneType, container ) ).orElseGet( () -> new PluralAttributeKeySourceImpl( sourceMappingDocument(), - pluralAttributeJaxbMapping.isInverse() ? - pluralAttributeJaxbMapping.getKey() : - pluralAttributeJaxbMapping.getKey(), + pluralAttributeJaxbMapping.getKey(), container ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/PluralAttributeKeySourceImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/PluralAttributeKeySourceImpl.java index 30caba025b..3a105cbe85 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/PluralAttributeKeySourceImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/PluralAttributeKeySourceImpl.java @@ -74,16 +74,31 @@ public List getColumnOrFormulaElements() { public PluralAttributeKeySourceImpl( MappingDocument mappingDocument, - final JaxbHbmManyToOneType jaxbKey, + final JaxbHbmKeyType jaxbKey, + final JaxbHbmManyToOneType jaxbManyToOne, final AttributeSourceContainer container) { super( mappingDocument ); - this.explicitFkName = StringHelper.nullIfEmpty( jaxbKey.getForeignKey() ); - this.referencedPropertyName = StringHelper.nullIfEmpty( jaxbKey.getPropertyRef() ); - this.cascadeDeletesAtFkLevel = jaxbKey.getOnDelete() != null - && "cascade".equals( jaxbKey.getOnDelete().value() ); - this.nullable = jaxbKey.isNotNull() == null || !jaxbKey.isNotNull(); - this.updateable = jaxbKey.isUpdate(); + this.explicitFkName = StringHelper.nullIfEmpty( jaxbManyToOne.getForeignKey() ); + this.referencedPropertyName = StringHelper.nullIfEmpty( jaxbManyToOne.getPropertyRef() ); + if ( jaxbKey.getOnDelete() == null ) { + this.cascadeDeletesAtFkLevel = jaxbManyToOne.getOnDelete() != null && "cascade".equals( jaxbManyToOne.getOnDelete().value() ); + } + else { + this.cascadeDeletesAtFkLevel = "cascade".equals( jaxbKey.getOnDelete().value() ); + } + if ( jaxbKey.isNotNull() == null ) { + this.nullable = jaxbManyToOne.isNotNull() == null || !jaxbManyToOne.isNotNull(); + } + else { + this.nullable = !jaxbKey.isNotNull(); + } + if ( jaxbKey.isUpdate() == null ) { + this.updateable = jaxbManyToOne.isUpdate(); + } + else { + this.updateable = jaxbKey.isUpdate(); + } this.valueSources = RelationalValueSourceHelper.buildValueSources( sourceMappingDocument(), @@ -106,7 +121,7 @@ public String getColumnAttribute() { @Override public List getColumnOrFormulaElements() { - return jaxbKey.getColumnOrFormula(); + return jaxbKey.getColumn(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java index 6dff2b157f..0d10e68146 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -396,59 +396,67 @@ private static void bindGenericGenerator(GenericGenerator def, MetadataBuildingC context.getMetadataCollector().addIdentifierGenerator( buildIdGenerator( def, context ) ); } - private static void bindQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { - { - SqlResultSetMapping ann = annotatedElement.getAnnotation( SqlResultSetMapping.class ); - QueryBinder.bindSqlResultSetMapping( ann, context, false ); - } - { - SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class ); - if ( ann != null ) { - for ( SqlResultSetMapping current : ann.value() ) { - QueryBinder.bindSqlResultSetMapping( current, context, false ); - } + private static void bindNamedJpaQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { + QueryBinder.bindSqlResultSetMapping( + annotatedElement.getAnnotation( SqlResultSetMapping.class ), + context, + false + ); + + final SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class ); + if ( ann != null ) { + for ( SqlResultSetMapping current : ann.value() ) { + QueryBinder.bindSqlResultSetMapping( current, context, false ); } } - { - NamedQuery ann = annotatedElement.getAnnotation( NamedQuery.class ); - QueryBinder.bindQuery( ann, context, false ); - } - { - org.hibernate.annotations.NamedQuery ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedQuery.class - ); - QueryBinder.bindQuery( ann, context ); - } - { - NamedQueries ann = annotatedElement.getAnnotation( NamedQueries.class ); - QueryBinder.bindQueries( ann, context, false ); - } - { - org.hibernate.annotations.NamedQueries ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedQueries.class - ); - QueryBinder.bindQueries( ann, context ); - } - { - NamedNativeQuery ann = annotatedElement.getAnnotation( NamedNativeQuery.class ); - QueryBinder.bindNativeQuery( ann, context, false ); - } - { - org.hibernate.annotations.NamedNativeQuery ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedNativeQuery.class - ); - QueryBinder.bindNativeQuery( ann, context ); - } - { - NamedNativeQueries ann = annotatedElement.getAnnotation( NamedNativeQueries.class ); - QueryBinder.bindNativeQueries( ann, context, false ); - } - { - org.hibernate.annotations.NamedNativeQueries ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedNativeQueries.class - ); - QueryBinder.bindNativeQueries( ann, context ); - } + + QueryBinder.bindQuery( + annotatedElement.getAnnotation( NamedQuery.class ), + context, + false + ); + + QueryBinder.bindQueries( + annotatedElement.getAnnotation( NamedQueries.class ), + context, + false + ); + + QueryBinder.bindNativeQuery( + annotatedElement.getAnnotation( NamedNativeQuery.class ), + context, + false + ); + + QueryBinder.bindNativeQueries( + annotatedElement.getAnnotation( NamedNativeQueries.class ), + context, + false + ); + } + + private static void bindQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { + bindNamedJpaQueries( annotatedElement, context ); + + QueryBinder.bindQuery( + annotatedElement.getAnnotation( org.hibernate.annotations.NamedQuery.class ), + context + ); + + QueryBinder.bindQueries( + annotatedElement.getAnnotation( org.hibernate.annotations.NamedQueries.class ), + context + ); + + QueryBinder.bindNativeQuery( + annotatedElement.getAnnotation( org.hibernate.annotations.NamedNativeQuery.class ), + context + ); + + QueryBinder.bindNativeQueries( + annotatedElement.getAnnotation( org.hibernate.annotations.NamedNativeQueries.class ), + context + ); // NamedStoredProcedureQuery handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bindNamedStoredProcedureQuery( diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/CopyIdentifierComponentSecondPass.java b/hibernate-core/src/main/java/org/hibernate/cfg/CopyIdentifierComponentSecondPass.java index c27bb73480..06f845e2ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/CopyIdentifierComponentSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/CopyIdentifierComponentSecondPass.java @@ -176,11 +176,7 @@ private Property createSimpleProperty( final Ejb3JoinColumn joinColumn; String logicalColumnName = null; if ( isExplicitReference ) { - final String columnName = column.getName(); - logicalColumnName = buildingContext.getMetadataCollector().getLogicalColumnName( - referencedPersistentClass.getTable(), - columnName - ); + logicalColumnName = column.getName(); //JPA 2 requires referencedColumnNames to be case insensitive joinColumn = columnByReferencedName.get( logicalColumnName.toLowerCase(Locale.ROOT ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 9a63210ab7..54bf6f5d18 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -92,6 +92,8 @@ import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.hibernate.type.descriptor.DateTimeUtils.*; @@ -130,6 +132,15 @@ public abstract class Dialect implements ConversionContext { * Characters used as closing for quoting SQL identifiers */ public static final String CLOSED_QUOTE = "`\"]"; + private static final Pattern SINGLE_QUOTE_PATTERN = Pattern.compile( + "'", + Pattern.LITERAL + ); + + private static final Pattern ESCAPE_CLOSING_COMMENT_PATTERN = Pattern.compile( "\\*/" ); + private static final Pattern ESCAPE_OPENING_COMMENT_PATTERN = Pattern.compile( "/\\*" ); + + public static final String TWO_SINGLE_QUOTES_REPLACEMENT = Matcher.quoteReplacement( "''" ); private final TypeNames typeNames = new TypeNames(); private final TypeNames hibernateTypeNames = new TypeNames(); @@ -1372,7 +1383,7 @@ public String getForUpdateString() { /** * Get the string to append to SELECT statements to acquire WRITE locks - * for this dialect. Location of the of the returned string is treated + * for this dialect. Location of the returned string is treated * the same as getForUpdateString. * * @param timeout in milliseconds, -1 for indefinite wait and 0 for no wait. @@ -1400,7 +1411,7 @@ public String getWriteLockString(String aliases, int timeout) { /** * Get the string to append to SELECT statements to acquire READ locks - * for this dialect. Location of the of the returned string is treated + * for this dialect. Location of the returned string is treated * the same as getForUpdateString. * * @param timeout in milliseconds, -1 for indefinite wait and 0 for no wait. @@ -1413,7 +1424,7 @@ public String getReadLockString(int timeout) { /** * Get the string to append to SELECT statements to acquire READ locks * for this dialect given the aliases of the columns to be read locked. - * Location of the of the returned string is treated + * Location of the returned string is treated * the same as getForUpdateString. * * @param aliases The columns to be read locked. @@ -1430,8 +1441,8 @@ public String getReadLockString(String aliases, int timeout) { * Does the FOR UPDATE OF clause accept a list of columns * instead of a list of table aliases? * - * @return True if the database FOR UPDATE OF clause takes - * a column list; false otherwise. + * @return True if the database supports FOR UPDATE OF syntax; + * false otherwise. */ public boolean forUpdateOfColumns() { // by default we report no support @@ -1528,7 +1539,7 @@ public String getForUpdateSkipLockedString(String aliases) { /** * Some dialects support an alternative means to SELECT FOR UPDATE, - * whereby a "lock hint" is appends to the table name in the from clause. + * whereby a "lock hint" is appended to the table name in the from clause. *

* contributed by Helge Schulz * @@ -1543,7 +1554,7 @@ public String appendLockHint(LockMode mode, String tableName) { } /** * Some dialects support an alternative means to SELECT FOR UPDATE, - * whereby a "lock hint" is appends to the table name in the from clause. + * whereby a "lock hint" is appended to the table name in the from clause. *

* contributed by Helge Schulz * @@ -1835,7 +1846,7 @@ public SQLExceptionConverter buildSQLExceptionConverter() { *

* It is strongly recommended that specific Dialect implementations override this * method, since interpretation of a SQL error is much more accurate when based on - * the a vendor-specific ErrorCode rather than the SQLState. + * the vendor-specific ErrorCode rather than the SQLState. *

* Specific Dialects may override to return whatever is most appropriate for that vendor. * @@ -2604,7 +2615,7 @@ public boolean supportsParametersInInsertSelect() { /** * Does this dialect require that references to result variables - * (i.e, select expresssion aliases) in an ORDER BY clause be + * (i.e, select expression aliases) in an ORDER BY clause be * replaced by column positions (1-origin) as defined * by the select clause? @@ -3019,6 +3030,8 @@ public CallableStatementSupport getCallableStatementSupport() { /** * By default interpret this based on DatabaseMetaData. + * + * @return The NameQualifierSupport. */ public NameQualifierSupport getNameQualifierSupport() { return null; @@ -3156,7 +3169,7 @@ public boolean supportsJdbcConnectionLobCreation(DatabaseMetaData databaseMetaDa * @return escaped String */ protected String escapeLiteral(String literal) { - return literal.replace("'", "''"); + return SINGLE_QUOTE_PATTERN.matcher( literal ).replaceAll( TWO_SINGLE_QUOTES_REPLACEMENT ); } /** @@ -3180,7 +3193,15 @@ public String addSqlHintOrComment( } protected String prependComment(String sql, String comment) { - return "/* " + comment + " */ " + sql; + return "/* " + escapeComment( comment ) + " */ " + sql; + } + + public static String escapeComment(String comment) { + if ( StringHelper.isNotEmpty( comment ) ) { + final String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher( comment ).replaceAll( "*\\\\/" ); + return ESCAPE_OPENING_COMMENT_PATTERN.matcher( escaped ).replaceAll( "/\\\\*" ); + } + return comment; } /** diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java b/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java index 47dd98fb3f..b71d4cdf35 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java @@ -19,6 +19,7 @@ import static org.hibernate.annotations.QueryHints.FLUSH_MODE; import static org.hibernate.annotations.QueryHints.FOLLOW_ON_LOCKING; import static org.hibernate.annotations.QueryHints.NATIVE_LOCKMODE; +import static org.hibernate.annotations.QueryHints.NATIVE_SPACES; import static org.hibernate.annotations.QueryHints.PASS_DISTINCT_THROUGH; import static org.hibernate.annotations.QueryHints.READ_ONLY; import static org.hibernate.annotations.QueryHints.TIMEOUT_HIBERNATE; @@ -26,8 +27,6 @@ /** * Defines the supported JPA query hints - * - * @author Steve Ebersole */ public class QueryHints { /** @@ -91,26 +90,23 @@ public class QueryHints { * * Note: Currently, attributes that are not specified are treated as FetchType.LAZY or FetchType.EAGER depending * on the attribute's definition in metadata, rather than forcing FetchType.LAZY. - * - * @deprecated (since 5.4) Use {@link GraphSemantic#FETCH}'s {@link GraphSemantic#getJpaHintName()} instead */ - @Deprecated public static final String HINT_FETCHGRAPH = GraphSemantic.FETCH.getJpaHintName(); /** * Hint providing a "loadgraph" EntityGraph. Attributes explicitly specified as AttributeNodes are treated as * FetchType.EAGER (via join fetch or subsequent select). Attributes that are not specified are treated as * FetchType.LAZY or FetchType.EAGER depending on the attribute's definition in metadata - * - * @deprecated (since 5.4) Use {@link GraphSemantic#LOAD}'s {@link GraphSemantic#getJpaHintName()} instead */ - @Deprecated public static final String HINT_LOADGRAPH = GraphSemantic.LOAD.getJpaHintName(); public static final String HINT_FOLLOW_ON_LOCKING = FOLLOW_ON_LOCKING; public static final String HINT_PASS_DISTINCT_THROUGH = PASS_DISTINCT_THROUGH; + public static final String HINT_NATIVE_SPACES = NATIVE_SPACES; + + private static final Set HINTS = buildHintsSet(); private static Set buildHintsSet() { @@ -127,6 +123,7 @@ private static Set buildHintsSet() { hints.add( HINT_NATIVE_LOCKMODE ); hints.add( HINT_FETCHGRAPH ); hints.add( HINT_LOADGRAPH ); + hints.add( HINT_NATIVE_SPACES ); return java.util.Collections.unmodifiableSet( hints ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java b/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java index 30d72fa7b4..6c24dc0fa7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java @@ -91,6 +91,7 @@ import static org.hibernate.jpa.QueryHints.HINT_FLUSH_MODE; import static org.hibernate.jpa.QueryHints.HINT_FOLLOW_ON_LOCKING; import static org.hibernate.jpa.QueryHints.HINT_LOADGRAPH; +import static org.hibernate.jpa.QueryHints.HINT_NATIVE_SPACES; import static org.hibernate.jpa.QueryHints.HINT_READONLY; import static org.hibernate.jpa.QueryHints.HINT_TIMEOUT; import static org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT; @@ -123,13 +124,11 @@ public AbstractProducedQuery( this.parameterMetadata = parameterMetadata; } - @Override public MutableQueryOptions getQueryOptions() { return queryOptions; } - @Override public FlushMode getHibernateFlushMode() { return getQueryOptions().getFlushMode(); @@ -1013,6 +1012,9 @@ else if ( JPA_SHARED_CACHE_STORE_MODE.equals( hintName ) ) { final CacheStoreMode storeMode = value != null ? CacheStoreMode.valueOf( value.toString() ) : null; applied = applyJpaCacheStoreMode( storeMode ); } + else if ( HINT_NATIVE_SPACES.equals( hintName ) ) { + applied = applyQuerySpaces( value ); + } else if ( QueryHints.HINT_NATIVE_LOCKMODE.equals( hintName ) ) { applied = applyNativeQueryLockMode( value ); } @@ -1064,6 +1066,12 @@ else if ( QueryHints.HINT_PASS_DISTINCT_THROUGH.equals( hintName ) ) { return this; } + protected boolean applyQuerySpaces(Object value) { + throw new IllegalStateException( + "Illegal attempt to apply native-query spaces to a non-native query" + ); + } + protected void handleUnrecognizedHint(String hintName, Object value) { MSG_LOGGER.debugf( "Skipping unsupported query hint [%s]", hintName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Delete.java b/hibernate-core/src/main/java/org/hibernate/sql/Delete.java index cd5affb8f3..61d97c4a3b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Delete.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Delete.java @@ -9,6 +9,8 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.hibernate.dialect.Dialect; + /** * An SQL DELETE statement * @@ -36,7 +38,7 @@ public Delete setTableName(String tableName) { public String toStatementString() { StringBuilder buf = new StringBuilder( tableName.length() + 10 ); if ( comment!=null ) { - buf.append( "/* " ).append(comment).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append( "delete from " ).append(tableName); if ( where != null || !primaryKeyColumns.isEmpty() || versionColumnName != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Insert.java b/hibernate-core/src/main/java/org/hibernate/sql/Insert.java index 93083a90e1..a50afc29b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Insert.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Insert.java @@ -94,7 +94,7 @@ public Insert setTableName(String tableName) { public String toStatementString() { StringBuilder buf = new StringBuilder( columns.size()*15 + tableName.length() + 10 ); if ( comment != null ) { - buf.append( "/* " ).append( comment ).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append("insert into ") .append(tableName); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/InsertSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/InsertSelect.java index afc5fbd061..ac6dcce678 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/InsertSelect.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/InsertSelect.java @@ -71,7 +71,7 @@ public String toStatementString() { StringBuilder buf = new StringBuilder( (columnNames.size() * 15) + tableName.length() + 10 ); if ( comment!=null ) { - buf.append( "/* " ).append( comment ).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append( "insert into " ).append( tableName ); if ( !columnNames.isEmpty() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/QuerySelect.java b/hibernate-core/src/main/java/org/hibernate/sql/QuerySelect.java index de9acd2e10..1811b213ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/QuerySelect.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/QuerySelect.java @@ -126,7 +126,7 @@ public void addOrderBy(String orderByString) { public String toQueryString() { StringBuilder buf = new StringBuilder( 50 ); if ( comment != null ) { - buf.append( "/* " ).append( comment ).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append( "select " ); if ( distinct ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Select.java b/hibernate-core/src/main/java/org/hibernate/sql/Select.java index 30a516dfb8..11e13d86b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Select.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Select.java @@ -42,7 +42,7 @@ public Select(Dialect dialect) { public String toStatementString() { StringBuilder buf = new StringBuilder(guesstimatedBufferSize); if ( StringHelper.isNotEmpty(comment) ) { - buf.append("/* ").append(comment).append(" */ "); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append("select ").append(selectClause) diff --git a/hibernate-core/src/main/java/org/hibernate/sql/SimpleSelect.java b/hibernate-core/src/main/java/org/hibernate/sql/SimpleSelect.java index 7baeef6366..9ab8ae9366 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/SimpleSelect.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/SimpleSelect.java @@ -151,7 +151,7 @@ public String toStatementString() { ); if ( comment != null ) { - buf.append( "/* " ).append( comment ).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append( "select " ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Update.java b/hibernate-core/src/main/java/org/hibernate/sql/Update.java index 7b9a3fdae8..5dcb0fd739 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Update.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Update.java @@ -166,7 +166,7 @@ public Update setWhere(String where) { public String toStatementString() { StringBuilder buf = new StringBuilder( (columns.size() * 15) + tableName.length() + 10 ); if ( comment!=null ) { - buf.append( "/* " ).append( comment ).append( " */ " ); + buf.append( "/* " ).append( Dialect.escapeComment( comment ) ).append( " */ " ); } buf.append( "update " ).append( tableName ).append( " set " ); boolean assignmentsAppended = false; diff --git a/hibernate-core/src/test/java/org/hibernate/test/collection/map/EmbeddableIndexTest.java b/hibernate-core/src/test/java/org/hibernate/test/collection/map/EmbeddableIndexTest.java new file mode 100644 index 0000000000..24420da26e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/collection/map/EmbeddableIndexTest.java @@ -0,0 +1,171 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.collection.map; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.MapKey; +import javax.persistence.OneToMany; + +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Andrea Boriero + */ +public class EmbeddableIndexTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { TheOne.class, TheMany.class }; + } + + @Before + public void setUp() { + inTransaction( + session -> { + TheOne one = new TheOne( "1" ); + session.save( one ); + + TheMapKey theMapKey = new TheMapKey( one ); + TheMany theMany = new TheMany( theMapKey ); + session.save( theMany ); + + Map map = new HashMap<>(); + map.put( theMapKey, theMany ); + one.setTheManys( map ); + } + ); + } + + @Test + public void testIt() { + inSession( + session -> { + TheOne one = session.get( TheOne.class, "1" ); + TheMapKey theMapKey = one.getTheManys().keySet().iterator().next(); + assertThat( theMapKey, is( notNullValue() ) ); + assertThat( theMapKey.getTheOne(), sameInstance( one ) ); + } + ); + + + } + + @Entity(name = "TheOne") + public static class TheOne { + private String id; + private String aString; + private Map theManys = new HashMap<>(); + + TheOne() { + } + + public TheOne(String id) { + this.id = id; + } + + @Id + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @OneToMany(mappedBy = "theMapKey.theOne") + @MapKey(name = "theMapKey") + public Map getTheManys() { + return theManys; + } + + public void setTheManys(Map theManys) { + this.theManys = theManys; + } + + public String getaString() { + return aString; + } + + public void setaString(String aString) { + this.aString = aString; + } + } + + @Embeddable + public static class TheMapKey implements Serializable { + private TheOne theOne; + private int anInt; + + TheMapKey() { + } + + public TheMapKey(TheOne theOne) { + this.theOne = theOne; + } + + @ManyToOne + public TheOne getTheOne() { + return theOne; + } + + public void setTheOne(TheOne theOne) { + this.theOne = theOne; + } + + public int getAnInt() { + return anInt; + } + + public void setAnInt(int anInt) { + this.anInt = anInt; + } + } + + @Entity(name = "TheMany") + public static class TheMany { + private TheMapKey theMapKey; + private String aString; + + TheMany() { + } + + public TheMany(TheMapKey theMapKey) { + this.theMapKey = theMapKey; + } + + @EmbeddedId + public TheMapKey getTheMapKey() { + return theMapKey; + } + + public void setTheMapKey(TheMapKey theMapKey) { + this.theMapKey = theMapKey; + } + + public String getaString() { + return aString; + } + + public void setaString(String aString) { + this.aString = aString; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity.java b/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity.java new file mode 100644 index 0000000000..7c425becc4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity.java @@ -0,0 +1,46 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.comments; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * @author Andrea Boriero + */ +@Entity +public class TestEntity { + @Id + private String id; + + private String value; + + public TestEntity() { + + } + + public TestEntity(String id, String value) { + this.id = id; + this.value = value; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity2.java b/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity2.java new file mode 100644 index 0000000000..58b626df60 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/comments/TestEntity2.java @@ -0,0 +1,37 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.comments; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * @author Andrea Boriero + */ +@Entity +public class TestEntity2 { + @Id + private String id; + + private String value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/comments/UseSqlCommentTest.java b/hibernate-core/src/test/java/org/hibernate/test/comments/UseSqlCommentTest.java new file mode 100644 index 0000000000..2bd6adf8c8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/comments/UseSqlCommentTest.java @@ -0,0 +1,111 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.comments; + +import java.util.List; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CompoundSelection; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Root; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; + +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertThat; + +/** + * @author Andrea Boriero + */ +public class UseSqlCommentTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { TestEntity.class, TestEntity2.class }; + } + + @Override + protected void addMappings(Map settings) { + settings.put( AvailableSettings.USE_SQL_COMMENTS, "true" ); + settings.put( AvailableSettings.FORMAT_SQL, "false" ); + } + + @Before + public void setUp() { + doInJPA( this::entityManagerFactory, entityManager -> { + TestEntity testEntity = new TestEntity(); + testEntity.setId( "test1" ); + testEntity.setValue( "value1" ); + entityManager.persist( testEntity ); + + TestEntity2 testEntity2 = new TestEntity2(); + testEntity2.setId( "test2" ); + testEntity2.setValue( "value2" ); + entityManager.persist( testEntity2 ); + } ); + } + + @Test + public void testIt() { + String appendLiteral = "*/select id as col_0_0_,value as col_1_0_ from testEntity2 where 1=1 or id=?--/*"; + doInJPA( this::entityManagerFactory, entityManager -> { + + List result = findUsingQuery( "test1", appendLiteral, entityManager ); + + TestEntity test1 = result.get( 0 ); + assertThat( test1.getValue(), is( appendLiteral ) ); + } ); + + doInJPA( this::entityManagerFactory, entityManager -> { + + List result = findUsingCriteria( "test1", appendLiteral, entityManager ); + + TestEntity test1 = result.get( 0 ); + assertThat( test1.getValue(), is( appendLiteral ) ); + } ); + } + + public List findUsingCriteria(String id, String appendLiteral, EntityManager entityManager) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery( TestEntity.class ); + Root root = criteria.from( TestEntity.class ); + + Path idPath = root.get( "id" ); + CompoundSelection selection = builder.construct( + TestEntity.class, + idPath, + builder.literal( appendLiteral ) + ); + criteria.select( selection ); + + criteria.where( builder.equal( idPath, builder.parameter( String.class, "where_id" ) ) ); + + TypedQuery query = entityManager.createQuery( criteria ); + query.setParameter( "where_id", id ); + return query.getResultList(); + } + + public List findUsingQuery(String id, String appendLiteral, EntityManager entityManager) { + TypedQuery query = + entityManager.createQuery( + "select new org.hibernate.test.comments.TestEntity(id, '" + + appendLiteral.replace( "'", "''" ) + + "') from TestEntity where id=:where_id", + TestEntity.class + ); + query.setParameter( "where_id", id ); + return query.getResultList(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/NestedIdClassDerivedIdentifiersTest.java b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/NestedIdClassDerivedIdentifiersTest.java new file mode 100644 index 0000000000..db4f779d57 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/NestedIdClassDerivedIdentifiersTest.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.mapping.hhh14276; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +import java.util.Map; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.query.sqm.mutation.internal.inline.InlineStrategy; + +import org.hibernate.test.mapping.hhh14276.entity.PlayerStat; +import org.hibernate.test.mapping.hhh14276.entity.Score; +import org.hibernate.testing.TestForIssue; +import org.junit.Before; +import org.junit.Test; + +@TestForIssue(jiraKey = "HHH-14276") +public class NestedIdClassDerivedIdentifiersTest extends BaseEntityManagerFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + PlayerStat.class, + Score.class + }; + } + + @Override + protected void addConfigOptions(Map options) { + options.put( AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, Boolean.TRUE ); + options.put( AvailableSettings.QUERY_MULTI_TABLE_MUTATION_STRATEGY, InlineStrategy.class.getName() ); + } + + @Before + public void setUp() { + doInJPA( this::entityManagerFactory, em -> + { + // do nothing + } ); + } + + @Test + public void testNestedIdClassDerivedIdentifiers() { + doInJPA( this::entityManagerFactory, em -> + { + // do nothing + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStat.java b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStat.java new file mode 100644 index 0000000000..48fe1dc884 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStat.java @@ -0,0 +1,82 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.mapping.hhh14276.entity; + +import java.io.Serializable; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "\"PlayerStats\"") +@IdClass(PlayerStatId.class) +public class PlayerStat implements Serializable { + + @Id + @Column(name = "player_id") + private Integer playerId; + + @Basic(optional = false) + @Column(name = "jersey_nbr") + private Integer jerseyNbr; + + @Id + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "game_id", referencedColumnName = "game_id") + @JoinColumn(name = "is_home", referencedColumnName = "is_home") + private Score score; + + public PlayerStat() { + } + + public Integer getGameId() { + return score.getGameId(); + } + + public void setGameId(Integer gameId) { + score.setGameId( gameId ); + } + + public Boolean getHome() { + return score.getHome(); + } + + public void setHome(Boolean home) { + score.setHome( home ); + } + + public Integer getPlayerId() { + return playerId; + } + + public void setPlayerId(Integer playerId) { + this.playerId = playerId; + } + + public Integer getJerseyNbr() { + return jerseyNbr; + } + + public void setJerseyNbr(Integer jerseyNbr) { + this.jerseyNbr = jerseyNbr; + } + + public Score getScore() { + return score; + } + + public void setScore(Score score) { + this.score = score; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStatId.java b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStatId.java new file mode 100644 index 0000000000..0b7b3455c7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/PlayerStatId.java @@ -0,0 +1,52 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.mapping.hhh14276.entity; + +import java.io.Serializable; + +public class PlayerStatId implements Serializable { + + private Integer playerId; + + // nested composite PK @IdClass: named like relationship in entity class + private ScoreId score; + + public PlayerStatId() { + } + + public Integer getGameId() { + return score.getGameId(); + } + + public void setGameId(Integer gameId) { + score.setGameId( gameId ); + } + + public Boolean getHome() { + return score.getHome(); + } + + public void setHome(Boolean home) { + score.setHome( home ); + } + + public Integer getPlayerId() { + return playerId; + } + + public void setPlayerId(Integer playerId) { + this.playerId = playerId; + } + + public ScoreId getScoreId() { + return score; + } + + public void setScoreId(ScoreId scoreId) { + this.score = scoreId; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/Score.java b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/Score.java new file mode 100644 index 0000000000..5c03991752 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/Score.java @@ -0,0 +1,73 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.mapping.hhh14276.entity; + +import java.io.Serializable; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; + +@Entity +@Table(name = "\"Scores\"") +@IdClass(ScoreId.class) +public class Score implements Serializable { + + @Id + @Column(name = "game_id") + private Integer gameId; + + @Id + @Column(name = "is_home") + private Boolean home; + + @Basic(optional = false) + @Column(name = "roster_id") + private Integer rosterId; + + @Basic + @Column(name = "final_score") + private Integer finalScore; + + public Score() { + } + + public Integer getGameId() { + return gameId; + } + + public void setGameId(Integer gameId) { + this.gameId = gameId; + } + + public Boolean getHome() { + return home; + } + + public void setHome(Boolean home) { + this.home = home; + } + + public Integer getRosterId() { + return rosterId; + } + + public void setRosterId(Integer rosterId) { + this.rosterId = rosterId; + } + + public Integer getFinalScore() { + return finalScore; + } + + public void setFinalScore(Integer finalScore) { + this.finalScore = finalScore; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/ScoreId.java b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/ScoreId.java new file mode 100644 index 0000000000..4b7e6d2162 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/mapping/hhh14276/entity/ScoreId.java @@ -0,0 +1,35 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.mapping.hhh14276.entity; + +import java.io.Serializable; + +public class ScoreId implements Serializable { + + private Integer gameId; + + private Boolean home; + + public ScoreId() { + } + + public Integer getGameId() { + return gameId; + } + + public void setGameId(Integer gameId) { + this.gameId = gameId; + } + + public Boolean getHome() { + return home; + } + + public void setHome(Boolean home) { + this.home = home; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/query/sql/SynchronizedSpaceTests.java b/hibernate-core/src/test/java/org/hibernate/test/query/sql/SynchronizedSpaceTests.java new file mode 100644 index 0000000000..2738d3cd10 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/query/sql/SynchronizedSpaceTests.java @@ -0,0 +1,401 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.query.sql; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.persistence.Cacheable; +import javax.persistence.Entity; +import javax.persistence.EntityResult; +import javax.persistence.Id; +import javax.persistence.Query; +import javax.persistence.QueryHint; +import javax.persistence.SqlResultSetMapping; +import javax.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NamedNativeQuery; +import org.hibernate.cache.spi.CacheImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.jpa.QueryHints; +import org.hibernate.query.sql.spi.NativeQueryImplementor; + +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Steve Ebersole + */ +public class SynchronizedSpaceTests extends BaseNonConfigCoreFunctionalTestCase { + @Test + public void testNonSyncedCachedScenario() { + // CachedEntity updated by native-query without adding query spaces + // - the outcome should be all cached data being invalidated + + checkUseCase( + "cached_entity", + query -> {}, + // the 2 CachedEntity entries should not be there + false + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from CachedEntity", CachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + private void checkUseCase( + String table, + Consumer queryConsumer, + boolean shouldExistAfter) { + + checkUseCase( + (session) -> { + final Query nativeQuery = session.createNativeQuery( "update " + table + " set name = 'updated'" ); + queryConsumer.accept( nativeQuery ); + return nativeQuery; + }, + Query::executeUpdate, + shouldExistAfter + ); + } + + private void checkUseCase( + Function queryProducer, + Consumer executor, + boolean shouldExistAfter) { + + // first, load both `CachedEntity` instances into the L2 cache + loadAll(); + + final CacheImplementor cacheSystem = sessionFactory().getCache(); + + // make sure they are there + assertThat( cacheSystem.containsEntity( CachedEntity.class, 1 ), is( true ) ); + assertThat( cacheSystem.containsEntity( CachedEntity.class, 2 ), is( true ) ); + + // create a query to update the specified table - allowing the passed consumer to register a space if needed + inTransaction( + session -> { + // notice the type is the JPA Query interface + final Query nativeQuery = queryProducer.apply( session ); + executor.accept( nativeQuery ); + } + ); + + // see if the entries exist based on the expectation + assertThat( cacheSystem.containsEntity( CachedEntity.class, 1 ), is( shouldExistAfter ) ); + assertThat( cacheSystem.containsEntity( CachedEntity.class, 2 ), is( shouldExistAfter ) ); + } + + @Test + public void testSyncedCachedScenario() { + final String tableName = "cached_entity"; + + checkUseCase( + tableName, + query -> ( (NativeQueryImplementor) query ).addSynchronizedQuerySpace( tableName ), + // the 2 CachedEntity entries should not be there + false + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from CachedEntity", CachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testNonSyncedNonCachedScenario() { + // NonCachedEntity updated by native-query without adding query spaces + // - the outcome should be all cached data being invalidated + + checkUseCase( + "non_cached_entity", + query -> {}, + // the 2 CachedEntity entries should not be there + false + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testSyncedNonCachedScenario() { + // NonCachedEntity updated by native-query with query spaces + // - the caches for CachedEntity are not invalidated - they are not affected by the specified query-space + + final String tableName = "non_cached_entity"; + + checkUseCase( + tableName, + query -> ( (NativeQueryImplementor) query ).addSynchronizedQuerySpace( tableName ), + // the 2 CachedEntity entries should still be there + true + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingHint() { + // same as `#testSyncedNonCachedScenario`, but here using the hint + + final String tableName = "non_cached_entity"; + + checkUseCase( + tableName, + query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, tableName ), + // the 2 CachedEntity entries should still be there + true + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingHintWithCollection() { + // same as `#testSyncedNonCachedScenario`, but here using the hint + + final String tableName = "non_cached_entity"; + final Set spaces = new HashSet<>(); + spaces.add( tableName ); + + checkUseCase( + tableName, + query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, spaces ), + // the 2 CachedEntity entries should still be there + true + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingHintWithArray() { + // same as `#testSyncedNonCachedScenario`, but here using the hint + + final String tableName = "non_cached_entity"; + final String[] spaces = { tableName }; + + checkUseCase( + tableName, + query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, spaces ), + // the 2 CachedEntity entries should still be there + true + ); + + // and of course, let's make sure the update happened :) + inTransaction( + session -> { + session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach( + cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) ) + ); + } + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingAnnotationWithReturnClass() { + checkUseCase( + (session) -> session.createNamedQuery( "NonCachedEntity_return_class" ), + Query::getResultList, + true + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingAnnotationWithResultSetMapping() { + checkUseCase( + (session) -> session.createNamedQuery( "NonCachedEntity_resultset_mapping" ), + Query::getResultList, + true + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingAnnotationWithSpaces() { + checkUseCase( + (session) -> session.createNamedQuery( "NonCachedEntity_spaces" ), + Query::getResultList, + true + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingJpaAnnotationWithNoResultMapping() { + checkUseCase( + (session) -> session.createNamedQuery( "NonCachedEntity_raw_jpa" ), + Query::getResultList, + true + ); + } + + @Test + public void testSyncedNonCachedScenarioUsingJpaAnnotationWithHint() { + checkUseCase( + (session) -> session.createNamedQuery( "NonCachedEntity_hint_jpa" ), + Query::getResultList, + true + ); + } + + private void loadAll() { + inTransaction( + session -> { + session.createQuery( "from CachedEntity" ).list(); + + // this one is not strictly needed since this entity is not cached. + // but it helps my OCD feel better to have it ;) + session.createQuery( "from NonCachedEntity" ).list(); + } + ); + } + + public void prepareTest() { + inTransaction( + session -> { + session.persist( new CachedEntity( 1, "first cached" ) ); + session.persist( new CachedEntity( 2, "second cached" ) ); + + session.persist( new NonCachedEntity( 1, "first non-cached" ) ); + session.persist( new NonCachedEntity( 2, "second non-cached" ) ); + } + ); + + cleanupCache(); + } + + public void cleanupTest() { + cleanupCache(); + + inTransaction( + session -> { + session.createQuery( "delete CachedEntity" ).executeUpdate(); + session.createQuery( "delete NonCachedEntity" ).executeUpdate(); + } + ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { CachedEntity.class, NonCachedEntity.class }; + } + + @Override + protected boolean overrideCacheStrategy() { + return false; + } + + @Entity( name = "CachedEntity" ) + @Table( name = "cached_entity" ) + @Cacheable( true ) + @Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) + public static class CachedEntity { + @Id + private Integer id; + private String name; + + public CachedEntity() { + } + + public CachedEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity( name = "NonCachedEntity" ) + @Table( name = "non_cached_entity" ) + @Cacheable( false ) + @NamedNativeQuery( + name = "NonCachedEntity_return_class", + query = "select * from non_cached_entity", + resultClass = NonCachedEntity.class + ) + @NamedNativeQuery( + name = "NonCachedEntity_resultset_mapping", + query = "select * from non_cached_entity", + resultSetMapping = "NonCachedEntity_resultset_mapping" + ) + @SqlResultSetMapping( + name = "NonCachedEntity_resultset_mapping", + entities = @EntityResult( entityClass = NonCachedEntity.class ) + ) + @NamedNativeQuery( + name = "NonCachedEntity_spaces", + query = "select * from non_cached_entity", + querySpaces = "non_cached_entity" + ) + @javax.persistence.NamedNativeQuery( + name = "NonCachedEntity_raw_jpa", + query = "select * from non_cached_entity" + ) + @javax.persistence.NamedNativeQuery( + name = "NonCachedEntity_hint_jpa", + query = "select * from non_cached_entity", + hints = { + @QueryHint( name = QueryHints.HINT_NATIVE_SPACES, value = "non_cached_entity" ) + } + ) + public static class NonCachedEntity { + @Id + private Integer id; + private String name; + + public NonCachedEntity() { + } + + public NonCachedEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + } +}