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 String customDeleteSql;
private final boolean deleteCallable;
private final boolean dynamicUpdate;
private final boolean dynamicInsert;
private final List<Integer> 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 );

View File

@ -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() );

View File

@ -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<TableMapping> tablesWithNonNullValues = new ArrayList<>();
public InsertValuesAnalysis(EntityMutationTarget mutationTarget, Object[] values) {

View File

@ -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 {

View File

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

View File

@ -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;
}
}
}

View File

@ -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(

View File

@ -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;
}