draft: elliminate the "not in" query that may 'explode' on SqlServer

This commit is contained in:
Roland Praml
2023-08-21 17:10:23 +02:00
parent 2a24ec3d49
commit 77aeda1753
6 changed files with 56 additions and 41 deletions
@@ -586,12 +586,12 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
/**
* Find child beans of the parent <code>id</code>.
*/
public abstract List<Object> findIdsByParentId(Object id, Transaction transaction, boolean hard);
public abstract List<Object> findIdsByParentId(Object id, Transaction transaction, boolean includeSoftDeletes);
/**
* Find child beans of the parent ids in <code>idList</code>.
*/
public abstract List<Object> findIdsByParentIdList(List<Object> idList, Transaction transaction, boolean hard);
public abstract List<Object> findIdsByParentIdList(List<Object> idList, Transaction transaction, boolean includeSoftDeletes);
}
@@ -311,23 +311,23 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
* Find the Id's of detail beans given a parent Id
*/
@Override
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean hard) {
return sqlHelp.findIdsByParentId(parentId, t, hard, null);
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes) {
return sqlHelp.findIdsByParentId(parentId, t, includeSoftDeletes, null);
}
/**
* Find the Id's of detail beans given a parent Id and optionally exclude detail IDs
*/
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean hard, Set<Object> excludeDetailIds) {
return sqlHelp.findIdsByParentId(parentId, t, hard, excludeDetailIds);
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes, Set<Object> excludeDetailIds) {
return sqlHelp.findIdsByParentId(parentId, t, includeSoftDeletes, excludeDetailIds);
}
/**
* Find the Id's of detail beans given a list of parent Id's.
*/
@Override
public List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, boolean hard) {
return sqlHelp.findIdsByParentIdList(parentIdList, t, hard);
public List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, boolean includeSoftDeletes) {
return sqlHelp.findIdsByParentIdList(parentIdList, t, includeSoftDeletes);
}
/**
@@ -124,33 +124,25 @@ class BeanPropertyAssocManySqlHelp<T> {
many.bindParentIdsIn(rawWhere, parentIds, query);
}
List<Object> findIdsByParentId(Object parentId, Transaction t, boolean hard, Set<Object> excludeDetailIds) {
List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes, Set<Object> excludeDetailIds) {
final SpiEbeanServer server = descriptor.ebeanServer();
final SpiQuery<?> query = many.newQuery(server);
many.bindParentIdEq(rawParentIdEQ(""), parentId, query);
if (hard) {
if (includeSoftDeletes) {
query.setIncludeSoftDeletes();
}
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
if (excludeDetailIds.size() > 1000) { // TODO: Wait for #3176
// if we hit the parameter limit, we must filter that on the java side.
// There is no easy way to batch "not in" queries.
// checkme: We could pass the first 1000-2000 params to the DB and filter the rest
List<Object> ret = server.findIds(query, t);
ret.removeIf(id -> excludeDetailIds.contains(id));
return ret;
}
query.where().not(query.getExpressionFactory().idIn(excludeDetailIds));
}
return server.findIds(query, t);
}
List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean hard) {
List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean includeSoftDeletes) {
final SpiEbeanServer server = descriptor.ebeanServer();
final SpiQuery<?> query = many.newQuery(server);
many.bindParentIdsIn(rawParentIdIN("", parentIds.size()), parentIds, query);
if (hard) {
if (includeSoftDeletes) {
query.setIncludeSoftDeletes();
}
return server.findIds(query, t);
@@ -254,21 +254,29 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
}
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean hard) {
@Override
public List<Object> findIdsByParentId(Object parentId, Transaction t, boolean includeSoftDeletes) {
String rawWhere = deriveWhereParentIdSql(false);
SpiEbeanServer server = server();
Query<?> q = server.find(type());
bindParentIdEq(rawWhere, parentId, q);
if (includeSoftDeletes) {
q.setIncludeSoftDeletes();
}
return server.findIds(q, t);
}
public List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean hard) {
@Override
public List<Object> findIdsByParentIdList(List<Object> parentIds, Transaction t, boolean includeSoftDeletes) {
String rawWhere = deriveWhereParentIdSql(true);
String inClause = idBinder().idInValueExpr(false, parentIds.size());
String expr = rawWhere + inClause;
SpiEbeanServer server = server();
Query<?> q = server.find(type());
bindParentIdsIn(expr, parentIds, q);
if (includeSoftDeletes) {
q.setIncludeSoftDeletes();
}
return server.findIds(q, t);
}
@@ -697,7 +697,7 @@ public final class DefaultPersister implements Persister {
if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) {
executeSqlUpdate(sqlDeleteChildren(expOne), transaction);
} else {
List<Object> childIds = findChildIds(expOne, true); // CHECMKE: do we need something for soft/hard delete here
List<Object> childIds = findChildIds(expOne, deleteMode.isHard());
if (childIds != null && !childIds.isEmpty()) {
deleteChildrenById(transaction, targetDesc, childIds, deleteMode);
}
@@ -746,7 +746,7 @@ public final class DefaultPersister implements Persister {
abstract SqlUpdate sqlDeleteChildren(BeanPropertyAssoc<?> prop);
abstract List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean hard);
abstract List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes);
abstract int deleteBeans();
}
@@ -783,8 +783,8 @@ public final class DefaultPersister implements Persister {
}
@Override
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean hard) {
return prop.findIdsByParentId(id, transaction, hard);
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes) {
return prop.findIdsByParentId(id, transaction, includeSoftDeletes);
}
int deleteBeans() {
@@ -834,8 +834,8 @@ public final class DefaultPersister implements Persister {
}
@Override
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean hard) {
return prop.findIdsByParentIdList(idList, transaction, hard);
List<Object> findChildIds(BeanPropertyAssoc<?> prop, boolean includeSoftDeletes) {
return prop.findIdsByParentIdList(idList, transaction, includeSoftDeletes);
}
int deleteBeans() {
@@ -1088,7 +1088,7 @@ public final class DefaultPersister implements Persister {
BeanDescriptor<?> targetDesc = many.targetDescriptor();
if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
if (targetDesc.isDeleteByStatement()
&& (excludeDetailIds == null || excludeDetailIds.size() <= 1000)) { // TODO wait for #3176
&& (excludeDetailIds == null || excludeDetailIds.size() <= maxDeleteBatch)) { // TODO wait for #3176
// Just delete all the children with one statement
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode);
@@ -1099,7 +1099,16 @@ public final class DefaultPersister implements Persister {
// ... and only using findIdsByParentId() when the many property isn't loaded
// Delete recurse using the Id values of the children
Object parentId = desc.getId(parentBean);
List<Object> idsByParentId = many.findIdsByParentId(parentId, t, deleteMode.isHard(), excludeDetailIds);
List<Object> idsByParentId;
if (excludeDetailIds == null || excludeDetailIds.size() <= maxDeleteBatch) { // TODO: Wait for #3176
idsByParentId = many.findIdsByParentId(parentId, t, deleteMode.isHard(), excludeDetailIds);
} else {
// if we hit the parameter limit, we must filter that on the java side.
// There is no easy way to batch "not in" queries.
// checkme: We could pass the first 1000-2000 params to the DB and filter the rest
idsByParentId = many.findIdsByParentId(parentId, t, deleteMode.isHard(), null);
idsByParentId.removeIf(id -> excludeDetailIds.contains(id));
}
if (!idsByParentId.isEmpty()) {
deleteChildrenById(t, targetDesc, idsByParentId, deleteMode);
}
@@ -50,11 +50,13 @@ class TestOrphanCollectionReplacement extends BaseTestCase {
long parentId = setup(2500); // we cannot make a "not in" query for so many params
List<String> sql = doUpdate(parentId, name -> !"c0".equals(name));
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
assertThat(sql).hasSize(4);
assertThat(sql.get(0)).contains("update coone_many set deleted=true where coone_id = ? and not ( id ");
assertThat(sql.get(1)).contains(" -- bind(");
assertThat(sql.get(2)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
assertThat(sql.get(3)).contains(" -- bind(");
// CHECKME: H2 would not require the batch mode here and could theoretically do it in fewer statements
assertThat(sql).hasSize(5);
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = false and t0.deleted = false; --bind");
assertThat(sql.get(1)).contains("update coone_many set deleted=true where id in (?)");
assertThat(sql.get(2)).contains(" -- bind(");
assertThat(sql.get(3)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
assertThat(sql.get(4)).contains(" -- bind(");
}
if (isSqlServer()) {
@@ -81,15 +83,19 @@ class TestOrphanCollectionReplacement extends BaseTestCase {
long parentId = setup(5000); // we will replace 2500 beans in this step
List<String> sql = doUpdate(parentId, name -> Integer.parseInt(name.substring(1)) >= 2500);
if (isH2() || isPostgresCompatible()) { // using deleted=true vs deleted=1
assertThat(sql).hasSize(4);
assertThat(sql.get(0)).contains("update coone_many set deleted=true where coone_id = ? and not ( id ");
assertThat(sql.get(1)).contains(" -- bind(");
assertThat(sql.get(2)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
assertThat(sql.get(3)).contains(" -- bind(");
// CHECKME: H2 would not require the batch mode here and could theoretically do it in fewer statements
assertThat(sql).hasSize(8);
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = false and t0.deleted = false; --bind"); // find all Ids
assertThat(sql.get(1)).contains("update coone_many set deleted=true where id in (?,?,?");
assertThat(sql.get(2)).contains(" -- bind(Array[1000]="); // update first 1000
assertThat(sql.get(3)).contains(" -- bind(Array[1000]="); // update second 1000
assertThat(sql.get(4)).contains("update coone_many set deleted=true where id in (?,?,?");
assertThat(sql.get(5)).contains(" -- bind(Array[500]="); // update last 500
assertThat(sql.get(6)).contains("insert into coone_many (coone_id, name, deleted) values (?,?,?)");
assertThat(sql.get(7)).contains(" -- bind(");
}
if (isSqlServer()) {
// filter mode
assertThat(sql).hasSize(7);
assertThat(sql.get(0)).contains("select t0.id from coone_many t0 where coone_id=? and t0.deleted = 0 and t0.deleted = 0; --bind"); // find all Ids
assertThat(sql.get(1)).contains("update coone_many set deleted=1 where id in (?,?,?");