HHH-12387 - Immutable entities can be updated via bulk update queries
This commit is contained in:
parent
ae0dfdc779
commit
b0e591f01d
|
@ -55,6 +55,7 @@ import org.hibernate.jpa.JpaCompliance;
|
|||
import org.hibernate.jpa.spi.JpaComplianceImpl;
|
||||
import org.hibernate.loader.BatchFetchStyle;
|
||||
import org.hibernate.proxy.EntityNotFoundDelegate;
|
||||
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
|
||||
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.GENERATE_STATISTICS;
|
||||
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.JDBC_TIME_ZONE;
|
||||
import static org.hibernate.cfg.AvailableSettings.JDBC_TYLE_PARAMS_ZERO_BASE;
|
||||
|
@ -229,6 +231,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
|
|||
private TimeZone jdbcTimeZone;
|
||||
private boolean queryParametersValidationEnabled;
|
||||
private LiteralHandlingMode criteriaLiteralHandlingMode;
|
||||
private ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode;
|
||||
|
||||
private Map<String, SQLFunction> sqlFunctions;
|
||||
|
||||
|
@ -469,6 +472,10 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
|
|||
configurationSettings,
|
||||
false
|
||||
);
|
||||
|
||||
this.immutableEntityUpdateQueryHandlingMode = ImmutableEntityUpdateQueryHandlingMode.interpret(
|
||||
configurationSettings.get( IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE )
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
|
@ -952,6 +959,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
|
|||
return this.criteriaLiteralHandlingMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
|
||||
return immutableEntityUpdateQueryHandlingMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean jdbcStyleParamsZeroBased() {
|
||||
return this.jdbcStyleParamsZeroBased;
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
|
|||
import org.hibernate.jpa.JpaCompliance;
|
||||
import org.hibernate.loader.BatchFetchStyle;
|
||||
import org.hibernate.proxy.EntityNotFoundDelegate;
|
||||
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.StatementInspector;
|
||||
|
@ -411,4 +412,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
|
|||
public boolean isFailOnPaginationOverCollectionFetchEnabled() {
|
||||
return delegate.isFailOnPaginationOverCollectionFetchEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
|
||||
return delegate.getImmutableEntityUpdateQueryHandlingMode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
|
|||
import org.hibernate.jpa.JpaCompliance;
|
||||
import org.hibernate.loader.BatchFetchStyle;
|
||||
import org.hibernate.proxy.EntityNotFoundDelegate;
|
||||
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
|
||||
import org.hibernate.query.criteria.LiteralHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
|
||||
import org.hibernate.resource.jdbc.spi.StatementInspector;
|
||||
|
@ -275,4 +276,8 @@ public interface SessionFactoryOptions {
|
|||
JpaCompliance getJpaCompliance();
|
||||
|
||||
boolean isFailOnPaginationOverCollectionFetchEnabled();
|
||||
|
||||
default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
|
||||
return ImmutableEntityUpdateQueryHandlingMode.WARNING;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,14 @@ import java.util.function.Supplier;
|
|||
|
||||
import javax.persistence.GeneratedValue;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.Transaction;
|
||||
import org.hibernate.boot.MetadataBuilder;
|
||||
import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence;
|
||||
import org.hibernate.cache.spi.TimestampsCacheFactory;
|
||||
import org.hibernate.internal.log.DeprecationLogger;
|
||||
import org.hibernate.jpa.JpaCompliance;
|
||||
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
|
||||
import org.hibernate.query.internal.ParameterMetadataImpl;
|
||||
import org.hibernate.resource.beans.container.spi.ExtendedBeanManager;
|
||||
import org.hibernate.resource.transaction.spi.TransactionCoordinator;
|
||||
|
@ -1861,4 +1863,24 @@ public interface AvailableSettings extends org.hibernate.jpa.AvailableSettings {
|
|||
* @since 5.2.13
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
|
|
@ -450,4 +450,8 @@ public class HQLQueryPlan implements Serializable {
|
|||
public boolean isSelect() {
|
||||
return !translators[0].isManipulationStatement();
|
||||
}
|
||||
|
||||
public boolean isUpdate() {
|
||||
return translators[0].isUpdateStatement();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.QueryNode;
|
||||
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.ASTUtil;
|
||||
import org.hibernate.hql.internal.ast.util.NodeTraverser;
|
||||
|
@ -503,6 +504,10 @@ public class QueryTranslatorImpl implements FilterTranslator {
|
|||
return sqlAst.needsExecutor();
|
||||
}
|
||||
@Override
|
||||
public boolean isUpdateStatement() {
|
||||
return SqlTokenTypes.UPDATE == sqlAst.getStatementType();
|
||||
}
|
||||
@Override
|
||||
public void validateScrollability() throws HibernateException {
|
||||
// Impl Note: allows multiple collection fetches as long as the
|
||||
// entire fecthed graph still "points back" to a single
|
||||
|
|
|
@ -171,5 +171,9 @@ public interface QueryTranslator {
|
|||
|
||||
boolean isManipulationStatement();
|
||||
|
||||
default boolean isUpdateStatement() {
|
||||
return getQueryString().toLowerCase().trim().startsWith( "update" );
|
||||
}
|
||||
|
||||
Class getDynamicInstantiationResultType();
|
||||
}
|
||||
|
|
|
@ -1800,5 +1800,8 @@ public interface CoreMessageLogger extends BasicLogger {
|
|||
id = 486)
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import java.sql.Clob;
|
|||
import java.sql.Connection;
|
||||
import java.sql.NClob;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -164,6 +165,7 @@ import org.hibernate.procedure.ProcedureCallMemento;
|
|||
import org.hibernate.procedure.UnknownSqlResultSetMappingException;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.hibernate.proxy.LazyInitializer;
|
||||
import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
|
||||
import org.hibernate.query.Query;
|
||||
import org.hibernate.query.criteria.internal.compile.CompilableCriteria;
|
||||
import org.hibernate.query.criteria.internal.compile.CriteriaCompiler;
|
||||
|
@ -1505,6 +1507,8 @@ public final class SessionImpl
|
|||
HQLQueryPlan plan = getQueryPlan( query, false );
|
||||
autoFlushIfRequired( plan.getQuerySpaces() );
|
||||
|
||||
verifyImmutableEntityUpdate( plan );
|
||||
|
||||
boolean success = false;
|
||||
int result = 0;
|
||||
try {
|
||||
|
@ -1518,6 +1522,42 @@ public final class SessionImpl
|
|||
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
|
||||
public int executeNativeUpdate(
|
||||
NativeSQLQuerySpecification nativeQuerySpecification,
|
||||
|
|
|
@ -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''."
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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() );
|
||||
} );
|
||||
}
|
||||
}
|
|
@ -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() );
|
||||
} );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue