HHH-17587 Setting to null a property from a @SecondaryTable and @DynamicUpdate deletes the whole entry from database

This commit is contained in:
Andrea Boriero 2024-01-30 13:50:41 +01:00 committed by Christian Beikov
parent 663e5c0206
commit 225740bce5
8 changed files with 77 additions and 13 deletions

View File

@ -3508,6 +3508,8 @@ public abstract class AbstractEntityPersister
private final Expectation deleteExpectation; private final Expectation deleteExpectation;
private final String customDeleteSql; private final String customDeleteSql;
private final boolean deleteCallable; private final boolean deleteCallable;
private final boolean dynamicUpdate;
private final boolean dynamicInsert;
private final List<Integer> attributeIndexes = new ArrayList<>(); private final List<Integer> attributeIndexes = new ArrayList<>();
@ -3527,7 +3529,9 @@ public abstract class AbstractEntityPersister
boolean cascadeDeleteEnabled, boolean cascadeDeleteEnabled,
Expectation deleteExpectation, Expectation deleteExpectation,
String customDeleteSql, String customDeleteSql,
boolean deleteCallable) { boolean deleteCallable,
boolean dynamicUpdate,
boolean dynamicInsert) {
this.tableName = tableName; this.tableName = tableName;
this.relativePosition = relativePosition; this.relativePosition = relativePosition;
this.keyMapping = keyMapping; this.keyMapping = keyMapping;
@ -3544,6 +3548,8 @@ public abstract class AbstractEntityPersister
this.deleteExpectation = deleteExpectation; this.deleteExpectation = deleteExpectation;
this.customDeleteSql = customDeleteSql; this.customDeleteSql = customDeleteSql;
this.deleteCallable = deleteCallable; this.deleteCallable = deleteCallable;
this.dynamicUpdate = dynamicUpdate;
this.dynamicInsert = dynamicInsert;
} }
private EntityTableMapping build() { private EntityTableMapping build() {
@ -3564,7 +3570,9 @@ public abstract class AbstractEntityPersister
cascadeDeleteEnabled, cascadeDeleteEnabled,
deleteExpectation, deleteExpectation,
customDeleteSql, customDeleteSql,
deleteCallable deleteCallable,
dynamicUpdate,
dynamicInsert
); );
} }
} }
@ -3621,7 +3629,9 @@ public abstract class AbstractEntityPersister
isTableCascadeDeleteEnabled( relativePosition ), isTableCascadeDeleteEnabled( relativePosition ),
deleteExpectations[ relativePosition ], deleteExpectations[ relativePosition ],
customDeleteSql, customDeleteSql,
deleteCallable[ relativePosition ] deleteCallable[ relativePosition ],
entityMetamodel.isDynamicUpdate(),
entityMetamodel.isDynamicInsert()
); );
tableBuilderMap.put( tableExpression, tableMappingBuilder ); tableBuilderMap.put( tableExpression, tableMappingBuilder );

View File

@ -65,14 +65,33 @@ public class EntityTableMapping implements TableMapping {
boolean cascadeDeleteEnabled, boolean cascadeDeleteEnabled,
Expectation deleteExpectation, Expectation deleteExpectation,
String deleteCustomSql, String deleteCustomSql,
boolean deleteCallable) { boolean deleteCallable,
boolean dynamicUpdate,
boolean dynamicInsert) {
this.tableName = tableName; this.tableName = tableName;
this.relativePosition = relativePosition; this.relativePosition = relativePosition;
this.keyMapping = keyMapping; this.keyMapping = keyMapping;
this.attributeIndexes = attributeIndexes; this.attributeIndexes = attributeIndexes;
this.insertDetails = new MutationDetails( MutationType.INSERT, insertExpectation, insertCustomSql, insertCallable ); this.insertDetails = new MutationDetails(
this.updateDetails = new MutationDetails( MutationType.UPDATE, updateExpectation, updateCustomSql, updateCallable ); MutationType.INSERT,
this.deleteDetails = new MutationDetails( MutationType.DELETE, deleteExpectation, deleteCustomSql, deleteCallable ); 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 ) { if ( isOptional ) {
flags.set( Flag.OPTIONAL.ordinal() ); flags.set( Flag.OPTIONAL.ordinal() );

View File

@ -153,7 +153,7 @@ public class InsertCoordinatorStandard extends AbstractMutationCoordinator imple
return foundStateDependentGenerator; return foundStateDependentGenerator;
} }
protected static class InsertValuesAnalysis implements ValuesAnalysis { public static class InsertValuesAnalysis implements ValuesAnalysis {
private final List<TableMapping> tablesWithNonNullValues = new ArrayList<>(); private final List<TableMapping> tablesWithNonNullValues = new ArrayList<>();
public InsertValuesAnalysis(EntityMutationTarget mutationTarget, Object[] values) { public InsertValuesAnalysis(EntityMutationTarget mutationTarget, Object[] values) {

View File

@ -308,7 +308,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple
); );
//noinspection StatementWithEmptyBody //noinspection StatementWithEmptyBody
if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() ) { if ( valuesAnalysis.tablesNeedingUpdate.isEmpty() && valuesAnalysis.tablesNeedingDynamicUpdate.isEmpty() ) {
// nothing to do // nothing to do
return null; return null;
} }
@ -972,7 +972,9 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple
if ( tableMapping.isOptional() if ( tableMapping.isOptional()
&& !valuesAnalysis.tablesWithNonNullValues.contains( tableMapping ) ) { && !valuesAnalysis.tablesWithNonNullValues.contains( tableMapping ) ) {
// the table is optional, and we have null values for all of its columns // 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; return false;
} }
else { else {

View File

@ -40,6 +40,8 @@ public interface UpdateValuesAnalysis extends ValuesAnalysis {
*/ */
TableSet getTablesWithPreviousNonNullValues(); TableSet getTablesWithPreviousNonNullValues();
TableSet getTablesNeedingDynamicUpdate();
/** /**
* Descriptors for the analysis of each attribute * Descriptors for the analysis of each attribute
*/ */

View File

@ -81,16 +81,33 @@ public interface TableMapping extends TableDetails {
private final Expectation expectation; private final Expectation expectation;
private final String customSql; private final String customSql;
private final boolean callable; private final boolean callable;
private final boolean dynamicMutation;
public MutationDetails( public MutationDetails(
MutationType mutationType, MutationType mutationType,
Expectation expectation, Expectation expectation,
String customSql, String customSql,
boolean callable) { boolean callable) {
this(
mutationType,
expectation,
customSql,
callable,
false
);
}
public MutationDetails(
MutationType mutationType,
Expectation expectation,
String customSql,
boolean callable,
boolean dynamicMutation) {
this.mutationType = mutationType; this.mutationType = mutationType;
this.expectation = expectation; this.expectation = expectation;
this.customSql = customSql; this.customSql = customSql;
this.callable = callable; this.callable = callable;
this.dynamicMutation = dynamicMutation;
} }
/** /**
@ -122,5 +139,10 @@ public interface TableMapping extends TableDetails {
public boolean isCallable() { public boolean isCallable() {
return callable; return callable;
} }
public boolean isDynamicMutation() {
return dynamicMutation;
}
} }
} }

View File

@ -13,6 +13,7 @@ import java.util.function.Consumer;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jdbc.Expectation; import org.hibernate.jdbc.Expectation;
import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.persister.entity.mutation.EntityMutationTarget;
import org.hibernate.persister.entity.mutation.InsertCoordinatorStandard;
import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBinder;
import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperation;
@ -128,9 +129,16 @@ public class OptionalTableUpdate
@Override @Override
public MutationOperation createMutationOperation(ValuesAnalysis valuesAnalysis, SessionFactoryImplementor factory) { public MutationOperation createMutationOperation(ValuesAnalysis valuesAnalysis, SessionFactoryImplementor factory) {
if ( getMutatingTable().getTableMapping().getInsertDetails().getCustomSql() != null final TableMapping tableMapping = getMutatingTable().getTableMapping();
|| getMutatingTable().getTableMapping().getDeleteDetails().getCustomSql() != null ) { 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 // 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 new OptionalTableUpdateOperation( getMutationTarget(), this, factory );
} }
return factory.getJdbcServices().getDialect().createOptionalTableUpdateOperation( return factory.getJdbcServices().getDialect().createOptionalTableUpdateOperation(

View File

@ -127,7 +127,8 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio
ValuesAnalysis incomingValuesAnalysis, ValuesAnalysis incomingValuesAnalysis,
SharedSessionContractImplementor session) { SharedSessionContractImplementor session) {
final UpdateValuesAnalysis valuesAnalysis = (UpdateValuesAnalysis) incomingValuesAnalysis; final UpdateValuesAnalysis valuesAnalysis = (UpdateValuesAnalysis) incomingValuesAnalysis;
if ( !valuesAnalysis.getTablesNeedingUpdate().contains( tableMapping ) ) { if ( !valuesAnalysis.getTablesNeedingUpdate().contains( tableMapping )
&& !valuesAnalysis.getTablesNeedingDynamicUpdate().contains( tableMapping ) ) {
return; return;
} }