diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index e48d2ffd8e..3d2fbc4151 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -3508,6 +3508,8 @@ public abstract class AbstractEntityPersister private final Expectation deleteExpectation; private final String customDeleteSql; private final boolean deleteCallable; + private final boolean dynamicUpdate; + private final boolean dynamicInsert; private final List attributeIndexes = new ArrayList<>(); @@ -3527,7 +3529,9 @@ public abstract class AbstractEntityPersister boolean cascadeDeleteEnabled, Expectation deleteExpectation, String customDeleteSql, - boolean deleteCallable) { + boolean deleteCallable, + boolean dynamicUpdate, + boolean dynamicInsert) { this.tableName = tableName; this.relativePosition = relativePosition; this.keyMapping = keyMapping; @@ -3544,6 +3548,8 @@ public abstract class AbstractEntityPersister this.deleteExpectation = deleteExpectation; this.customDeleteSql = customDeleteSql; this.deleteCallable = deleteCallable; + this.dynamicUpdate = dynamicUpdate; + this.dynamicInsert = dynamicInsert; } private EntityTableMapping build() { @@ -3564,7 +3570,9 @@ public abstract class AbstractEntityPersister cascadeDeleteEnabled, deleteExpectation, customDeleteSql, - deleteCallable + deleteCallable, + dynamicUpdate, + dynamicInsert ); } } @@ -3621,7 +3629,9 @@ public abstract class AbstractEntityPersister isTableCascadeDeleteEnabled( relativePosition ), deleteExpectations[ relativePosition ], customDeleteSql, - deleteCallable[ relativePosition ] + deleteCallable[ relativePosition ], + entityMetamodel.isDynamicUpdate(), + entityMetamodel.isDynamicInsert() ); tableBuilderMap.put( tableExpression, tableMappingBuilder ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java index 7ca7a489d2..e73f3e68d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java @@ -65,14 +65,33 @@ public class EntityTableMapping implements TableMapping { boolean cascadeDeleteEnabled, Expectation deleteExpectation, String deleteCustomSql, - boolean deleteCallable) { + boolean deleteCallable, + boolean dynamicUpdate, + boolean dynamicInsert) { this.tableName = tableName; this.relativePosition = relativePosition; this.keyMapping = keyMapping; this.attributeIndexes = attributeIndexes; - this.insertDetails = new MutationDetails( MutationType.INSERT, insertExpectation, insertCustomSql, insertCallable ); - this.updateDetails = new MutationDetails( MutationType.UPDATE, updateExpectation, updateCustomSql, updateCallable ); - this.deleteDetails = new MutationDetails( MutationType.DELETE, deleteExpectation, deleteCustomSql, deleteCallable ); + this.insertDetails = new MutationDetails( + MutationType.INSERT, + insertExpectation, + insertCustomSql, + insertCallable, + dynamicInsert + ); + this.updateDetails = new MutationDetails( + MutationType.UPDATE, + updateExpectation, + updateCustomSql, + updateCallable, + dynamicUpdate + ); + this.deleteDetails = new MutationDetails( + MutationType.DELETE, + deleteExpectation, + deleteCustomSql, + deleteCallable + ); if ( isOptional ) { flags.set( Flag.OPTIONAL.ordinal() ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java index 212ae8dd3c..6b74d8593c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinatorStandard.java @@ -153,7 +153,7 @@ public class InsertCoordinatorStandard extends AbstractMutationCoordinator imple return foundStateDependentGenerator; } - protected static class InsertValuesAnalysis implements ValuesAnalysis { + public static class InsertValuesAnalysis implements ValuesAnalysis { private final List tablesWithNonNullValues = new ArrayList<>(); public InsertValuesAnalysis(EntityMutationTarget mutationTarget, Object[] values) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index 1ae8377546..5df81f6a51 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -308,7 +308,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple ); //noinspection StatementWithEmptyBody - if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() ) { + if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() && valuesAnalysis.tablesNeedingDynamicUpdate.isEmpty() ) { // nothing to do return null; } @@ -972,7 +972,9 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple if ( tableMapping.isOptional() && !valuesAnalysis.tablesWithNonNullValues.contains( tableMapping ) ) { // the table is optional, and we have null values for all of its columns - // todo (6.0) : technically we might need to delete row here + if ( valuesAnalysis.dirtyAttributeIndexes.length > 0 ) { + return true; + } return false; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateValuesAnalysis.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateValuesAnalysis.java index 9b5eff3533..02e75326ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateValuesAnalysis.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateValuesAnalysis.java @@ -40,6 +40,8 @@ public interface UpdateValuesAnalysis extends ValuesAnalysis { */ TableSet getTablesWithPreviousNonNullValues(); + TableSet getTablesNeedingDynamicUpdate(); + /** * Descriptors for the analysis of each attribute */ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/TableMapping.java b/hibernate-core/src/main/java/org/hibernate/sql/model/TableMapping.java index 60de5d041c..50eebe1b27 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/TableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/TableMapping.java @@ -81,16 +81,33 @@ public interface TableMapping extends TableDetails { private final Expectation expectation; private final String customSql; private final boolean callable; + private final boolean dynamicMutation; public MutationDetails( MutationType mutationType, Expectation expectation, String customSql, boolean callable) { + this( + mutationType, + expectation, + customSql, + callable, + false + ); + } + + public MutationDetails( + MutationType mutationType, + Expectation expectation, + String customSql, + boolean callable, + boolean dynamicMutation) { this.mutationType = mutationType; this.expectation = expectation; this.customSql = customSql; this.callable = callable; + this.dynamicMutation = dynamicMutation; } /** @@ -122,5 +139,10 @@ public interface TableMapping extends TableDetails { public boolean isCallable() { return callable; } + + public boolean isDynamicMutation() { + return dynamicMutation; + } } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java index fd429a8102..d428d9ed8e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java @@ -13,6 +13,7 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.jdbc.Expectation; import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.persister.entity.mutation.InsertCoordinatorStandard; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.model.MutationOperation; @@ -128,9 +129,16 @@ public class OptionalTableUpdate @Override public MutationOperation createMutationOperation(ValuesAnalysis valuesAnalysis, SessionFactoryImplementor factory) { - if ( getMutatingTable().getTableMapping().getInsertDetails().getCustomSql() != null - || getMutatingTable().getTableMapping().getDeleteDetails().getCustomSql() != null ) { + final TableMapping tableMapping = getMutatingTable().getTableMapping(); + assert ! (valuesAnalysis instanceof InsertCoordinatorStandard.InsertValuesAnalysis ); + if ( tableMapping.getInsertDetails().getCustomSql() != null + || tableMapping.getInsertDetails().isDynamicMutation() + || tableMapping.getDeleteDetails().getCustomSql() != null + || tableMapping.getUpdateDetails().getCustomSql() != null + || tableMapping.getUpdateDetails().isDynamicMutation() ) { // Fallback to the optional table mutation operation because we have to execute user specified SQL + // and with dynamic update insert we have to avoid using merge for optional table because + // it can cause the deletion of the row when an attribute is set to null return new OptionalTableUpdateOperation( getMutationTarget(), this, factory ); } return factory.getJdbcServices().getDialect().createOptionalTableUpdateOperation( diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java index bc0d018f62..25fa3ef0cf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java @@ -127,7 +127,8 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio ValuesAnalysis incomingValuesAnalysis, SharedSessionContractImplementor session) { final UpdateValuesAnalysis valuesAnalysis = (UpdateValuesAnalysis) incomingValuesAnalysis; - if ( !valuesAnalysis.getTablesNeedingUpdate().contains( tableMapping ) ) { + if ( !valuesAnalysis.getTablesNeedingUpdate().contains( tableMapping ) + && !valuesAnalysis.getTablesNeedingDynamicUpdate().contains( tableMapping ) ) { return; }