HHH-12387 - Immutable entities can be updated via bulk update queries

This commit is contained in:
Vlad Mihalcea 2018-03-22 12:14:29 +02:00
parent ae0dfdc779
commit b0e591f01d
12 changed files with 297 additions and 1 deletions

View File

@ -55,6 +55,7 @@ import org.hibernate.jpa.JpaCompliance;
import org.hibernate.jpa.spi.JpaComplianceImpl; import org.hibernate.jpa.spi.JpaComplianceImpl;
import org.hibernate.loader.BatchFetchStyle; import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.criteria.LiteralHandlingMode; import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.resource.jdbc.spi.StatementInspector;
@ -88,6 +89,7 @@ import static org.hibernate.cfg.AvailableSettings.FAIL_ON_PAGINATION_OVER_COLLEC
import static org.hibernate.cfg.AvailableSettings.FLUSH_BEFORE_COMPLETION; import static org.hibernate.cfg.AvailableSettings.FLUSH_BEFORE_COMPLETION;
import static org.hibernate.cfg.AvailableSettings.GENERATE_STATISTICS; import static org.hibernate.cfg.AvailableSettings.GENERATE_STATISTICS;
import static org.hibernate.cfg.AvailableSettings.HQL_BULK_ID_STRATEGY; import static org.hibernate.cfg.AvailableSettings.HQL_BULK_ID_STRATEGY;
import static org.hibernate.cfg.AvailableSettings.IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE;
import static org.hibernate.cfg.AvailableSettings.INTERCEPTOR; import static org.hibernate.cfg.AvailableSettings.INTERCEPTOR;
import static org.hibernate.cfg.AvailableSettings.JDBC_TIME_ZONE; import static org.hibernate.cfg.AvailableSettings.JDBC_TIME_ZONE;
import static org.hibernate.cfg.AvailableSettings.JDBC_TYLE_PARAMS_ZERO_BASE; import static org.hibernate.cfg.AvailableSettings.JDBC_TYLE_PARAMS_ZERO_BASE;
@ -229,6 +231,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private TimeZone jdbcTimeZone; private TimeZone jdbcTimeZone;
private boolean queryParametersValidationEnabled; private boolean queryParametersValidationEnabled;
private LiteralHandlingMode criteriaLiteralHandlingMode; private LiteralHandlingMode criteriaLiteralHandlingMode;
private ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode;
private Map<String, SQLFunction> sqlFunctions; private Map<String, SQLFunction> sqlFunctions;
@ -469,6 +472,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
configurationSettings, configurationSettings,
false false
); );
this.immutableEntityUpdateQueryHandlingMode = ImmutableEntityUpdateQueryHandlingMode.interpret(
configurationSettings.get( IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE )
);
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -952,6 +959,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
return this.criteriaLiteralHandlingMode; return this.criteriaLiteralHandlingMode;
} }
@Override
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return immutableEntityUpdateQueryHandlingMode;
}
@Override @Override
public boolean jdbcStyleParamsZeroBased() { public boolean jdbcStyleParamsZeroBased() {
return this.jdbcStyleParamsZeroBased; return this.jdbcStyleParamsZeroBased;

View File

@ -25,6 +25,7 @@ import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.jpa.JpaCompliance; import org.hibernate.jpa.JpaCompliance;
import org.hibernate.loader.BatchFetchStyle; import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.criteria.LiteralHandlingMode; import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.resource.jdbc.spi.StatementInspector;
@ -411,4 +412,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
public boolean isFailOnPaginationOverCollectionFetchEnabled() { public boolean isFailOnPaginationOverCollectionFetchEnabled() {
return delegate.isFailOnPaginationOverCollectionFetchEnabled(); return delegate.isFailOnPaginationOverCollectionFetchEnabled();
} }
@Override
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return delegate.getImmutableEntityUpdateQueryHandlingMode();
}
} }

View File

@ -31,6 +31,7 @@ import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.jpa.JpaCompliance; import org.hibernate.jpa.JpaCompliance;
import org.hibernate.loader.BatchFetchStyle; import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.criteria.LiteralHandlingMode; import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.resource.jdbc.spi.StatementInspector;
@ -275,4 +276,8 @@ public interface SessionFactoryOptions {
JpaCompliance getJpaCompliance(); JpaCompliance getJpaCompliance();
boolean isFailOnPaginationOverCollectionFetchEnabled(); boolean isFailOnPaginationOverCollectionFetchEnabled();
default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return ImmutableEntityUpdateQueryHandlingMode.WARNING;
}
} }

View File

@ -10,12 +10,14 @@ import java.util.function.Supplier;
import javax.persistence.GeneratedValue; import javax.persistence.GeneratedValue;
import org.hibernate.HibernateException;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.hibernate.boot.MetadataBuilder; import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence; import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence;
import org.hibernate.cache.spi.TimestampsCacheFactory; import org.hibernate.cache.spi.TimestampsCacheFactory;
import org.hibernate.internal.log.DeprecationLogger; import org.hibernate.internal.log.DeprecationLogger;
import org.hibernate.jpa.JpaCompliance; import org.hibernate.jpa.JpaCompliance;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.internal.ParameterMetadataImpl; import org.hibernate.query.internal.ParameterMetadataImpl;
import org.hibernate.resource.beans.container.spi.ExtendedBeanManager; import org.hibernate.resource.beans.container.spi.ExtendedBeanManager;
import org.hibernate.resource.transaction.spi.TransactionCoordinator; import org.hibernate.resource.transaction.spi.TransactionCoordinator;
@ -1861,4 +1863,24 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings {
* @since 5.2.13 * @since 5.2.13
*/ */
String FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH = "hibernate.query.fail_on_pagination_over_collection_fetch"; String FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH = "hibernate.query.fail_on_pagination_over_collection_fetch";
/**
* This setting defines how {@link org.hibernate.annotations.Immutable} entities are handled when executing a
* bulk update {@link javax.persistence.Query}.
*
* By default, the ({@link ImmutableEntityUpdateQueryHandlingMode#WARNING}) mode is used, meaning that
* a warning log message is issued when an {@link org.hibernate.annotations.Immutable} entity
* is to be updated via a bulk update statement.
*
* If the ({@link ImmutableEntityUpdateQueryHandlingMode#EXCEPTION}) mode is used, then a
* {@link HibernateException} is thrown instead.
* </p>
* Valid options are defined by the {@link ImmutableEntityUpdateQueryHandlingMode} enum.
* </p>
* The default value is {@link ImmutableEntityUpdateQueryHandlingMode#WARNING}
*
* @since 5.2.17
* @see org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode
*/
String IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE = "hibernate.query.immutable_entity_update_query_handling_mode";
} }

View File

@ -450,4 +450,8 @@ public class HQLQueryPlan implements Serializable {
public boolean isSelect() { public boolean isSelect() {
return !translators[0].isManipulationStatement(); return !translators[0].isManipulationStatement();
} }
public boolean isUpdate() {
return translators[0].isUpdateStatement();
}
} }

View File

@ -38,6 +38,7 @@ import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.InsertStatement; import org.hibernate.hql.internal.ast.tree.InsertStatement;
import org.hibernate.hql.internal.ast.tree.QueryNode; import org.hibernate.hql.internal.ast.tree.QueryNode;
import org.hibernate.hql.internal.ast.tree.Statement; import org.hibernate.hql.internal.ast.tree.Statement;
import org.hibernate.hql.internal.ast.tree.UpdateStatement;
import org.hibernate.hql.internal.ast.util.ASTPrinter; import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.hql.internal.ast.util.ASTUtil; import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.hql.internal.ast.util.NodeTraverser; import org.hibernate.hql.internal.ast.util.NodeTraverser;
@ -503,6 +504,10 @@ public class QueryTranslatorImpl implements FilterTranslator {
return sqlAst.needsExecutor(); return sqlAst.needsExecutor();
} }
@Override @Override
public boolean isUpdateStatement() {
return SqlTokenTypes.UPDATE == sqlAst.getStatementType();
}
@Override
public void validateScrollability() throws HibernateException { public void validateScrollability() throws HibernateException {
// Impl Note: allows multiple collection fetches as long as the // Impl Note: allows multiple collection fetches as long as the
// entire fecthed graph still "points back" to a single // entire fecthed graph still "points back" to a single

View File

@ -171,5 +171,9 @@ public interface QueryTranslator {
boolean isManipulationStatement(); boolean isManipulationStatement();
default boolean isUpdateStatement() {
return getQueryString().toLowerCase().trim().startsWith( "update" );
}
Class getDynamicInstantiationResultType(); Class getDynamicInstantiationResultType();
} }

View File

@ -1800,5 +1800,8 @@ public interface CoreMessageLogger extends BasicLogger {
id = 486) id = 486)
void agroalProviderClassNotFound(); void agroalProviderClassNotFound();
@LogMessage(level = WARN)
@Message(value = "The query: [%s] attempts to update an immutable entity: %s",
id = 487)
void immutableEntityUpdateQuery(String sourceQuery, String querySpaces);
} }

View File

@ -17,6 +17,7 @@ import java.sql.Clob;
import java.sql.Connection; import java.sql.Connection;
import java.sql.NClob; import java.sql.NClob;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -164,6 +165,7 @@ import org.hibernate.procedure.ProcedureCallMemento;
import org.hibernate.procedure.UnknownSqlResultSetMappingException; import org.hibernate.procedure.UnknownSqlResultSetMappingException;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.Query; import org.hibernate.query.Query;
import org.hibernate.query.criteria.internal.compile.CompilableCriteria; import org.hibernate.query.criteria.internal.compile.CompilableCriteria;
import org.hibernate.query.criteria.internal.compile.CriteriaCompiler; import org.hibernate.query.criteria.internal.compile.CriteriaCompiler;
@ -1505,6 +1507,8 @@ public final class SessionImpl
HQLQueryPlan plan = getQueryPlan( query, false ); HQLQueryPlan plan = getQueryPlan( query, false );
autoFlushIfRequired( plan.getQuerySpaces() ); autoFlushIfRequired( plan.getQuerySpaces() );
verifyImmutableEntityUpdate( plan );
boolean success = false; boolean success = false;
int result = 0; int result = 0;
try { try {
@ -1518,6 +1522,42 @@ public final class SessionImpl
return result; return result;
} }
private void verifyImmutableEntityUpdate(HQLQueryPlan plan) {
if ( plan.isUpdate() ) {
for ( EntityPersister entityPersister : getSessionFactory().getMetamodel().entityPersisters().values() ) {
if ( !entityPersister.isMutable() ) {
List<Serializable> entityQuerySpaces = new ArrayList<>(
Arrays.asList( entityPersister.getQuerySpaces() )
);
entityQuerySpaces.retainAll( plan.getQuerySpaces() );
if ( !entityQuerySpaces.isEmpty() ) {
ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode = getSessionFactory()
.getSessionFactoryOptions()
.getImmutableEntityUpdateQueryHandlingMode();
String querySpaces = Arrays.toString( entityQuerySpaces.toArray() );
switch ( immutableEntityUpdateQueryHandlingMode ) {
case WARNING:
log.immutableEntityUpdateQuery(plan.getSourceQuery(), querySpaces);
break;
case EXCEPTION:
throw new HibernateException(
"The query: [" + plan.getSourceQuery() + "] attempts to update an immutable entity: " + querySpaces
);
default:
throw new UnsupportedOperationException(
"The "+ immutableEntityUpdateQueryHandlingMode + " is not supported!"
);
}
}
}
}
}
}
@Override @Override
public int executeNativeUpdate( public int executeNativeUpdate(
NativeSQLQuerySpecification nativeQuerySpecification, NativeSQLQuerySpecification nativeQuerySpecification,

View File

@ -0,0 +1,57 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.query;
import org.hibernate.HibernateException;
/**
* This enum defines how {@link org.hibernate.annotations.Immutable} entities are handled when executing a
* bulk update statement.
*
* By default, the ({@link ImmutableEntityUpdateQueryHandlingMode#WARNING}) mode is used, meaning that
* a warning log message is issued when an {@link org.hibernate.annotations.Immutable} entity
* is to be updated via a bulk update statement.
*
* If the ({@link ImmutableEntityUpdateQueryHandlingMode#EXCEPTION}) mode is used, then a
* {@link HibernateException} is thrown instead.
*
* @author Vlad Mihalcea
*/
public enum ImmutableEntityUpdateQueryHandlingMode {
WARNING,
EXCEPTION;
/**
* Interpret the configured {@link ImmutableEntityUpdateQueryHandlingMode} value.
* Valid values are either a {@link ImmutableEntityUpdateQueryHandlingMode} object or its String representation.
* For string values, the matching is case insensitive,
* so you can use either {@code warning} or {@code exception} (case insensitive).
*
* @param mode configured {@link ImmutableEntityUpdateQueryHandlingMode} representation
* @return associated {@link ImmutableEntityUpdateQueryHandlingMode} object
*/
public static ImmutableEntityUpdateQueryHandlingMode interpret(Object mode) {
if ( mode == null ) {
return WARNING;
}
else if ( mode instanceof ImmutableEntityUpdateQueryHandlingMode ) {
return (ImmutableEntityUpdateQueryHandlingMode) mode;
}
else if ( mode instanceof String ) {
for ( ImmutableEntityUpdateQueryHandlingMode value : values() ) {
if ( value.name().equalsIgnoreCase( (String) mode ) ) {
return value;
}
}
}
throw new HibernateException(
"Unrecognized immutable_entity_update_query_handling_mode value : " + mode
+ ". Supported values include 'warning' and 'exception''."
);
}
}

View File

@ -0,0 +1,68 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.annotations.immutable;
import java.util.Map;
import org.hibernate.HibernateException;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.hibernate.testing.logger.Triggerable;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* @author Vlad Mihalcea
*/
@TestForIssue( jiraKey = "HHH-12387" )
public class ImmutableEntityUpdateQueryHandlingModeExceptionTest extends BaseNonConfigCoreFunctionalTestCase {
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] { Country.class, State.class, Photo.class };
}
@Override
protected void addSettings(Map settings) {
settings.put( AvailableSettings.IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE, "exception" );
}
@Test
public void testBulkUpdate(){
Country _country = doInHibernate( this::sessionFactory, session -> {
Country country = new Country();
country.setName("Germany");
session.persist(country);
return country;
} );
try {
doInHibernate( this::sessionFactory, session -> {
session.createQuery(
"update Country " +
"set name = :name" )
.setParameter( "name", "N/A" )
.executeUpdate();
} );
fail("Should throw HibernateException");
}
catch (HibernateException e) {
assertEquals( "The query: [update Country set name = :name] attempts to update an immutable entity: [Country]", e.getMessage() );
}
doInHibernate( this::sessionFactory, session -> {
Country country = session.find(Country.class, _country.getId());
assertEquals( "Germany", country.getName() );
} );
}
}

View File

@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.annotations.immutable;
import org.hibernate.HibernateException;
import org.hibernate.boot.model.process.internal.ScanningCoordinator;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.SessionImpl;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.hibernate.testing.logger.LoggerInspectionRule;
import org.hibernate.testing.logger.Triggerable;
import org.junit.Rule;
import org.junit.Test;
import org.jboss.logging.Logger;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* @author Vlad Mihalcea
*/
@TestForIssue( jiraKey = "HHH-12387" )
public class ImmutableEntityUpdateQueryHandlingModeWarningTest extends BaseNonConfigCoreFunctionalTestCase {
@Rule
public LoggerInspectionRule logInspection = new LoggerInspectionRule(
Logger.getMessageLogger( CoreMessageLogger.class, SessionImpl.class.getName() ) );
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] { Country.class, State.class, Photo.class };
}
@Test
public void testBulkUpdate(){
Country _country = doInHibernate( this::sessionFactory, session -> {
Country country = new Country();
country.setName("Germany");
session.persist(country);
return country;
} );
Triggerable triggerable = logInspection.watchForLogMessages( "HHH000487" );
triggerable.reset();
doInHibernate( this::sessionFactory, session -> {
session.createQuery(
"update Country " +
"set name = :name" )
.setParameter( "name", "N/A" )
.executeUpdate();
} );
assertEquals( "HHH000487: The query: [update Country set name = :name] attempts to update an immutable entity: [Country]", triggerable.triggerMessage() );
doInHibernate( this::sessionFactory, session -> {
Country country = session.find(Country.class, _country.getId());
assertEquals( "N/A", country.getName() );
} );
}
}