Skip to content

Commit 5420ba2

Browse files
JaeYeon KimJaeYeon Kim
authored andcommitted
GH-1978 JdbcAggregateOperations delete by query
Changed the steps as follows: 1. Lock the target rows using SELECT ... FOR UPDATE based on the query conditions. 2. Delete sub-entities by leveraging a subquery that selects the matching root rows. 3. Delete the root entities using the same subquery criteria. Signed-off-by: JaeYeon Kim <JaeYeon.Kim@ibm.com>
1 parent 2f97ff2 commit 5420ba2

File tree

13 files changed

+264
-175
lines changed

13 files changed

+264
-175
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
102102
executionContext.executeBatchDeleteRoot(batchDeleteRoot);
103103
} else if (action instanceof DbAction.DeleteAllRoot<?> deleteAllRoot) {
104104
executionContext.executeDeleteAllRoot(deleteAllRoot);
105-
} else if (action instanceof DbAction.DeleteRootByIdIn<?> deleteRootByIdIn) {
106-
executionContext.executeDeleteRootByIdIn(deleteRootByIdIn);
107-
} else if (action instanceof DbAction.DeleteByRootIdIn<?> deleteByRootIdIn) {
108-
executionContext.executeDeleteByRootIdIn(deleteByRootIdIn);
105+
} else if (action instanceof DbAction.DeleteRootByQuery<?> deleteRootByQuery) {
106+
executionContext.excuteDeleteRootByQuery(deleteRootByQuery);
107+
} else if (action instanceof DbAction.DeleteByQuery<?> deleteByQuery) {
108+
executionContext.excuteDeleteByQuery(deleteByQuery);
109109
} else if (action instanceof DbAction.AcquireLockRoot<?> acquireLockRoot) {
110110
executionContext.executeAcquireLock(acquireLockRoot);
111111
} else if (action instanceof DbAction.AcquireLockAllRoot<?> acquireLockAllRoot) {

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.data.mapping.PersistentPropertyPath;
4141
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
4242
import org.springframework.data.relational.core.conversion.DbAction;
43-
import org.springframework.data.relational.core.conversion.SelectIdsDbActionExecutionResult;
4443
import org.springframework.data.relational.core.conversion.DbActionExecutionResult;
4544
import org.springframework.data.relational.core.conversion.IdValueSource;
4645
import org.springframework.data.relational.core.mapping.AggregatePath;
@@ -74,7 +73,6 @@ class JdbcAggregateChangeExecutionContext {
7473
private final DataAccessStrategy accessStrategy;
7574

7675
private final Map<DbAction<?>, DbActionExecutionResult> results = new LinkedHashMap<>();
77-
private final Map<DbAction.SelectIds<?>, SelectIdsDbActionExecutionResult> selectIdsDbActionExecutionResult = new LinkedHashMap<>();
7876

7977
JdbcAggregateChangeExecutionContext(JdbcConverter converter, DataAccessStrategy accessStrategy) {
8078

@@ -172,32 +170,14 @@ <T> void executeDeleteAll(DbAction.DeleteAll<T> delete) {
172170
accessStrategy.deleteAll(delete.getPropertyPath());
173171
}
174172

175-
<T> void executeDeleteRootByIdIn(DbAction.DeleteRootByIdIn<T> deleteRootByIdIn) {
176-
SelectIdsDbActionExecutionResult result = getRequiredSelectIdsResult(deleteRootByIdIn.getSelectIdsAction());
173+
<T> void excuteDeleteRootByQuery(DbAction.DeleteRootByQuery<T> deleteRootByQuery) {
177174

178-
List<Object> rootIds = new ArrayList<>(result.getSelectedIds());
179-
if (rootIds.isEmpty()) {
180-
return;
181-
}
182-
accessStrategy.delete(rootIds, deleteRootByIdIn.getEntityType());
175+
accessStrategy.deleteRootByQuery(deleteRootByQuery.getQuery(), deleteRootByQuery.getEntityType());
183176
}
184177

185-
<T> void executeDeleteByRootIdIn(DbAction.DeleteByRootIdIn<T> deleteByRootIdIn) {
186-
SelectIdsDbActionExecutionResult result = getRequiredSelectIdsResult(deleteByRootIdIn.getSelectIdsAction());
178+
<T> void excuteDeleteByQuery(DbAction.DeleteByQuery<T> deleteByQuery) {
187179

188-
List<Object> rootIds = new ArrayList<>(result.getSelectedIds());
189-
if (rootIds.isEmpty()) {
190-
return;
191-
}
192-
accessStrategy.delete(rootIds, deleteByRootIdIn.getPropertyPath());
193-
}
194-
195-
private SelectIdsDbActionExecutionResult getRequiredSelectIdsResult(DbAction.SelectIds selectIdsAction) {
196-
SelectIdsDbActionExecutionResult result = selectIdsDbActionExecutionResult.get(selectIdsAction);
197-
if (result == null) {
198-
throw new IllegalArgumentException("Expected SelectIdsDbActionExecutionResult for given selectIdsAction but found none");
199-
}
200-
return result;
180+
accessStrategy.deleteByQuery(deleteByQuery.getQuery(), deleteByQuery.getPropertyPath());
201181
}
202182

203183
<T> void executeAcquireLock(DbAction.AcquireLockRoot<T> acquireLock) {
@@ -209,10 +189,7 @@ <T> void executeAcquireLockAllRoot(DbAction.AcquireLockAllRoot<T> acquireLock) {
209189
}
210190

211191
<T> void executeAcquireLockRootByQuery(DbAction.AcquireLockAllRootByQuery<T> acquireLock) {
212-
213-
List<?> rootIds = accessStrategy.acquireLockAndFindIdsByQuery(acquireLock.getQuery(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
214-
215-
selectIdsDbActionExecutionResult.put(acquireLock, new SelectIdsDbActionExecutionResult(rootIds, acquireLock));
192+
accessStrategy.acquireLockByQuery(acquireLock.getQuery(), LockMode.PESSIMISTIC_WRITE, acquireLock.getEntityType());
216193
}
217194

218195
private void add(DbActionExecutionResult result) {

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
110110
collectVoid(das -> das.deleteAll(propertyPath));
111111
}
112112

113+
@Override
114+
public void deleteRootByQuery(Query query, Class<?> domainType) {
115+
collectVoid(das -> das.deleteRootByQuery(query, domainType));
116+
}
117+
118+
@Override
119+
public void deleteByQuery(Query query, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
120+
collectVoid(das -> das.deleteByQuery(query, propertyPath));
121+
}
122+
113123
@Override
114124
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
115125
collectVoid(das -> das.acquireLockById(id, lockMode, domainType));
@@ -121,8 +131,8 @@ public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
121131
}
122132

123133
@Override
124-
public <T> List<?> acquireLockAndFindIdsByQuery(Query query, LockMode lockMode, Class<T> domainType) {
125-
return collect(das -> das.acquireLockAndFindIdsByQuery(query, lockMode, domainType));
134+
public <T> void acquireLockByQuery(Query query, LockMode lockMode, Class<T> domainType) {
135+
collectVoid(das -> das.acquireLockByQuery(query, lockMode, domainType));
126136
}
127137

128138
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@ public interface DataAccessStrategy extends ReadingDataAccessStrategy, RelationR
178178
*/
179179
void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
180180

181+
/**
182+
* Deletes all root entities of the given domain type that match the given {@link Query}.
183+
*
184+
* @param query the query specifying which rows to delete. Must not be {@code null}.
185+
* @param domainType the domain type of the entity. Must not be {@code null}.
186+
*/
187+
void deleteRootByQuery(Query query, Class<?> domainType);
188+
189+
/**
190+
* Deletes entities reachable via the given {@link PersistentPropertyPath} from root entities that match the given {@link Query}.
191+
*
192+
* @param query the query specifying which root entities to consider for deleting related entities. Must not be {@code null}.
193+
* @param propertyPath Leading from the root object to the entities to be deleted. Must not be {@code null}.
194+
*/
195+
void deleteByQuery(Query query, PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
196+
181197
/**
182198
* Acquire a lock on the aggregate specified by id.
183199
*
@@ -196,16 +212,14 @@ public interface DataAccessStrategy extends ReadingDataAccessStrategy, RelationR
196212
<T> void acquireLockAll(LockMode lockMode, Class<T> domainType);
197213

198214
/**
199-
* Acquire a lock on all aggregates that match the given {@link Query} and return their identifiers.
200-
* The resulting SQL will include a {@code SELECT id FROM … WHERE … (LOCK CLAUSE)} to retrieve and lock the matching rows.
215+
* Acquire a lock on all aggregates that match the given {@link Query}.
201216
*
202217
* @param query the query specifying which entities to lock. Must not be {@code null}.
203218
* @param lockMode the lock mode to apply to the query (e.g. {@code FOR UPDATE}). Must not be {@code null}.
204219
* @param domainType the domain type of the entities to be locked. Must not be {@code null}.
205220
* @param <T> the type of the domain entity.
206-
* @return a {@link List} of ids corresponding to the rows locked by the query.
207221
*/
208-
<T> List<?> acquireLockAndFindIdsByQuery(Query query, LockMode lockMode, Class<T> domainType);
222+
<T> void acquireLockByQuery(Query query, LockMode lockMode, Class<T> domainType);
209223

210224
/**
211225
* Counts the rows in the table representing the given domain type.

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.springframework.data.relational.core.sql.LockMode;
4040
import org.springframework.data.relational.core.sql.SqlIdentifier;
4141
import org.springframework.jdbc.core.RowMapper;
42-
import org.springframework.jdbc.core.SingleColumnRowMapper;
4342
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
4443
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4544
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
@@ -245,6 +244,30 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
245244
operations.getJdbcOperations().update(sql(getBaseType(propertyPath)).createDeleteAllSql(propertyPath));
246245
}
247246

247+
@Override
248+
public void deleteRootByQuery(Query query, Class<?> domainType) {
249+
250+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
251+
String deleteSql = sql(domainType).createDeleteByQuery(query, parameterSource);
252+
253+
operations.update(deleteSql, parameterSource);
254+
}
255+
256+
@Override
257+
public void deleteByQuery(Query query, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
258+
259+
RelationalPersistentEntity<?> rootEntity = context.getRequiredPersistentEntity(getBaseType(propertyPath));
260+
261+
RelationalPersistentProperty referencingProperty = propertyPath.getLeafProperty();
262+
263+
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
264+
265+
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
266+
String deleteSql = sql(rootEntity.getType()).createDeleteInSubselectByPath(query, parameterSource, propertyPath);
267+
268+
operations.update(deleteSql, parameterSource);
269+
}
270+
248271
@Override
249272
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
250273

@@ -262,25 +285,12 @@ public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
262285
}
263286

264287
@Override
265-
public <T> List<?> acquireLockAndFindIdsByQuery(Query query, LockMode lockMode, Class<T> domainType) {
288+
public <T> void acquireLockByQuery(Query query, LockMode lockMode, Class<T> domainType) {
266289

267290
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
268-
String acquireLockByQuerySql = sql(domainType).getAcquireLockAndFindIdsByQuery(query, parameterSource, lockMode);
291+
String acquireLockByQuerySql = sql(domainType).getAcquireLockByQuery(query, parameterSource, lockMode);
269292

270-
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(domainType);
271-
RelationalPersistentProperty idProperty = entity.getRequiredIdProperty();
272-
273-
return operations.query(acquireLockByQuerySql, parameterSource, getIdRowMapper(idProperty));
274-
}
275-
276-
private RowMapper<?> getIdRowMapper(RelationalPersistentProperty idProperty) {
277-
RelationalPersistentEntity<?> complexId = context.getPersistentEntity(idProperty.getType());
278-
279-
if (complexId == null) {
280-
return SingleColumnRowMapper.newInstance(idProperty.getType(), converter.getConversionService());
281-
} else {
282-
return new EntityRowMapper<>(context.getRequiredPersistentEntity(idProperty.getType()), converter);
283-
}
293+
operations.query(acquireLockByQuerySql, parameterSource, ResultSet::next);
284294
}
285295

286296
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
110110
delegate.deleteAll(propertyPath);
111111
}
112112

113+
@Override
114+
public void deleteRootByQuery(Query query, Class<?> domainType) {
115+
delegate.deleteRootByQuery(query, domainType);
116+
}
117+
118+
@Override
119+
public void deleteByQuery(Query query, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
120+
delegate.deleteByQuery(query, propertyPath);
121+
}
122+
113123
@Override
114124
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
115125
delegate.acquireLockById(id, lockMode, domainType);
@@ -121,8 +131,8 @@ public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
121131
}
122132

123133
@Override
124-
public <T> List<?> acquireLockAndFindIdsByQuery(Query query, LockMode lockMode, Class<T> domainType) {
125-
return delegate.acquireLockAndFindIdsByQuery(query, lockMode, domainType);
134+
public <T> void acquireLockByQuery(Query query, LockMode lockMode, Class<T> domainType) {
135+
delegate.acquireLockByQuery(query, lockMode, domainType);
126136
}
127137

128138
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ String getAcquireLockAll(LockMode lockMode) {
386386
* @param lockMode Lock clause mode.
387387
* @return the SQL statement as a {@link String}. Guaranteed to be not {@literal null}.
388388
*/
389-
String getAcquireLockAndFindIdsByQuery(Query query, MapSqlParameterSource parameterSource, LockMode lockMode) {
389+
String getAcquireLockByQuery(Query query, MapSqlParameterSource parameterSource, LockMode lockMode) {
390390
return this.createAcquireLockByQuery(query, parameterSource, lockMode);
391391
}
392392

@@ -505,6 +505,76 @@ String createDeleteInByPath(PersistentPropertyPath<RelationalPersistentProperty>
505505
return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), this::inCondition);
506506
}
507507

508+
/**
509+
* Create a {@code DELETE FROM ... WHERE ...} SQL statement based on the given {@link Query}.
510+
*
511+
* @param query the query object defining filter criteria; must not be {@literal null}.
512+
* @param parameterSource the parameter bindings for the query; must not be {@literal null}.
513+
* @return the SQL DELETE statement as a {@link String}; guaranteed to be not {@literal null}.
514+
*/
515+
public String createDeleteByQuery(Query query, MapSqlParameterSource parameterSource) {
516+
Assert.notNull(parameterSource, "parameterSource must not be null");
517+
518+
Table table = this.getTable();
519+
520+
DeleteBuilder.DeleteWhere builder = Delete.builder()
521+
.from(table);
522+
523+
query.getCriteria()
524+
.filter(criteria -> !criteria.isEmpty())
525+
.ifPresent(criteria ->
526+
builder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity))
527+
);
528+
529+
return render(builder.build());
530+
}
531+
532+
/**
533+
* Creates a {@code DELETE} SQL query that targets a specific table defined by the given {@link PersistentPropertyPath},
534+
* and applies filtering using a subselect based on the provided {@link Query}.
535+
*
536+
* @param query the query object containing the filtering criteria; must not be {@literal null}.
537+
* @param parameterSource the source for parameter bindings used in the query; must not be {@literal null}.
538+
* @param propertyPath must not be {@literal null}.
539+
* @return the DELETE SQL statement as a {@link String}. Guaranteed to be not {@literal null}.
540+
*/
541+
public String createDeleteInSubselectByPath(Query query, MapSqlParameterSource parameterSource,
542+
PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
543+
544+
Assert.notNull(parameterSource, "parameterSource must not be null");
545+
546+
AggregatePath path = mappingContext.getAggregatePath(propertyPath);
547+
548+
return createDeleteByPathAndCriteria(path, columnMap -> {
549+
Select subSelect = createRootIdSubSelectByQuery(query, parameterSource);
550+
Collection<Column> columns = columnMap.values();
551+
Expression expression = columns.size() == 1 ? columns.iterator().next() : TupleExpression.create(columns);
552+
return Conditions.in(expression, subSelect);
553+
});
554+
}
555+
556+
/**
557+
* Creates a subselect that retrieves root entity IDs filtered by the given query.
558+
*/
559+
private Select createRootIdSubSelectByQuery(Query query, MapSqlParameterSource parameterSource) {
560+
561+
Table table = this.getTable();
562+
563+
SelectBuilder.SelectWhere selectBuilder = StatementBuilder
564+
.select(getIdColumns())
565+
.from(table);
566+
567+
query.getCriteria()
568+
.filter(criteria -> !criteria.isEmpty())
569+
.ifPresent(criteria ->
570+
selectBuilder.where(
571+
queryMapper.getMappedObject(parameterSource, criteria, table, entity)
572+
)
573+
);
574+
575+
return selectBuilder.build();
576+
}
577+
508578
/**
509579
* Constructs a where condition. The where condition will be of the form {@literal <columns> IN :bind-marker}
510580
*/
@@ -614,10 +684,18 @@ private String createAcquireLockByQuery(Query query, MapSqlParameterSource param
614684
Table table = this.getTable();
615685

616686
SelectBuilder.SelectWhere selectBuilder = StatementBuilder
617-
.select(getIdColumns())
687+
.select(getSingleNonNullColumn())
618688
.from(table);
619689

620-
Select select = applyQueryOnSelect(query, parameterSource, selectBuilder)
690+
query.getCriteria()
691+
.filter(criteria -> !criteria.isEmpty())
692+
.ifPresent(criteria ->
693+
selectBuilder.where(
694+
queryMapper.getMappedObject(parameterSource, criteria, table, entity)
695+
)
696+
);
697+
698+
Select select = selectBuilder
621699
.lock(lockMode)
622700
.build();
623701

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ public void deleteAll(PersistentPropertyPath<RelationalPersistentProperty> prope
231231
sqlSession().delete(statement, parameter);
232232
}
233233

234+
@Override
235+
public void deleteRootByQuery(Query query, Class<?> domainType) {
236+
throw new UnsupportedOperationException("Not implemented");
237+
}
238+
239+
@Override
240+
public void deleteByQuery(Query query, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
241+
throw new UnsupportedOperationException("Not implemented");
242+
}
243+
234244
@Override
235245
public <T> void acquireLockById(Object id, LockMode lockMode, Class<T> domainType) {
236246

@@ -255,7 +265,7 @@ public <T> void acquireLockAll(LockMode lockMode, Class<T> domainType) {
255265
}
256266

257267
@Override
258-
public <T> List<?> acquireLockAndFindIdsByQuery(Query query, LockMode lockMode, Class<T> domainType) {
268+
public <T> void acquireLockByQuery(Query query, LockMode lockMode, Class<T> domainType) {
259269
throw new UnsupportedOperationException("Not implemented");
260270
}
261271

0 commit comments

Comments
 (0)