From bcc4034371a6fcac0e9cbf72bcc7d288ddc2bab8 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 16 Aug 2023 17:39:10 +1200 Subject: [PATCH] Add Lists.partition() helper method and use in DefaultPersister batch deletion --- ebean-api/src/main/java/io/ebean/Lists.java | 33 ++++++++ .../src/test/java/io/ebean/ListsTest.java | 83 +++++++++++++++++++ .../server/persist/DefaultPersister.java | 15 ++-- 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/Lists.java create mode 100644 ebean-api/src/test/java/io/ebean/ListsTest.java diff --git a/ebean-api/src/main/java/io/ebean/Lists.java b/ebean-api/src/main/java/io/ebean/Lists.java new file mode 100644 index 000000000..a478877f2 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/Lists.java @@ -0,0 +1,33 @@ +package io.ebean; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper methods for Lists. + */ +public interface Lists { + + /** + * Partition the source List into sub-lists with a maximum size. + * + * @param max The max size of each partition + * @param source The source list + * @param The list element type + * @return List of partitions + */ + static List> partition(int max, List source) { + final int totalCount = source.size(); + if (totalCount <= max) { + return List.of(source); + } + final int numOfPartitions = (totalCount + max - 1) / max; // round up + final var dest = new ArrayList>(numOfPartitions); + for (int i = 0; i < numOfPartitions; i++) { + final int from = i * max; + final int to = Math.min(from + max, totalCount); + dest.add(source.subList(from, to)); + } + return dest; + } +} diff --git a/ebean-api/src/test/java/io/ebean/ListsTest.java b/ebean-api/src/test/java/io/ebean/ListsTest.java new file mode 100644 index 000000000..e719fda96 --- /dev/null +++ b/ebean-api/src/test/java/io/ebean/ListsTest.java @@ -0,0 +1,83 @@ +package io.ebean; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ListsTest { + + @Test + void partition_empty() { + List> partitions = Lists.partition(5, List.of()); + assertThat(partitions).hasSize(1); + assertThat(partitions.get(0)).hasSize(0); + } + + @Test + void partition_lt() { + List> partitions = Lists.partition(5, List.of(1, 2, 3, 4)); + assertThat(partitions).hasSize(1); + assertThat(partitions.get(0)).hasSize(4); + } + + @Test + void partition_eq() { + List> partitions = Lists.partition(5, List.of(1, 2, 3, 4, 5)); + assertThat(partitions).hasSize(1); + assertThat(partitions.get(0)).hasSize(5); + } + + @Test + void partition_gt() { + List> partitions = Lists.partition(5, List.of(1, 2, 3, 4, 5, 6)); + assertThat(partitions).hasSize(2); + assertThat(partitions.get(0)).hasSize(5); + assertThat(partitions.get(1)).hasSize(1); + assertThat(partitions.get(0)).containsExactly(1, 2, 3, 4, 5); + assertThat(partitions.get(1)).containsExactly(6); + } + + @Test + void partition_gt1_letters() { + var partitions = Lists.partition(3, List.of("a", "b", "c", "d")); + assertThat(partitions).hasSize(2); + assertThat(partitions.get(0)).hasSize(3); + assertThat(partitions.get(1)).hasSize(1); + assertThat(partitions.get(0)).containsExactly("a", "b", "c"); + assertThat(partitions.get(1)).containsExactly("d"); + } + + @Test + void partition_gt1a_letters() { + var partitions = Lists.partition(3, List.of("a", "b", "c", "d", "e")); + assertThat(partitions).hasSize(2); + assertThat(partitions.get(0)).hasSize(3); + assertThat(partitions.get(1)).hasSize(2); + assertThat(partitions.get(0)).containsExactly("a", "b", "c"); + assertThat(partitions.get(1)).containsExactly("d", "e"); + } + + @Test + void partition_eq2_letters() { + var partitions = Lists.partition(3, List.of("a", "b", "c", "d", "e", "f")); + assertThat(partitions).hasSize(2); + assertThat(partitions.get(0)).hasSize(3); + assertThat(partitions.get(1)).hasSize(3); + assertThat(partitions.get(0)).containsExactly("a", "b", "c"); + assertThat(partitions.get(1)).containsExactly("d", "e", "f"); + } + + @Test + void partition_gt2_letters() { + var partitions = Lists.partition(3, List.of("a", "b", "c", "d", "e", "f", "g")); + assertThat(partitions).hasSize(3); + assertThat(partitions.get(0)).hasSize(3); + assertThat(partitions.get(1)).hasSize(3); + assertThat(partitions.get(2)).hasSize(1); + assertThat(partitions.get(0)).containsExactly("a", "b", "c"); + assertThat(partitions.get(1)).containsExactly("d", "e", "f"); + assertThat(partitions.get(2)).containsExactly("g"); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 4d3efd92e..21705d202 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -647,16 +647,15 @@ public final class DefaultPersister implements Persister { * Delete by Id or a List of Id's. */ private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, DeleteMode deleteMode) { + if (idList == null) { + return deleteBatch(descriptor, id, null, transaction, deleteMode); + } int rows = 0; - if (maxInBinding > 0) { - // SqlServer has a 2100 parameter limit, so delete max 2000 ids at once - // this gives space up to 100 more query parameters. - while (idList != null && idList.size() > maxInBinding) { - rows += deleteBatch(descriptor, id, idList.subList(0, maxInBinding), transaction, deleteMode); - idList = idList.subList(maxInBinding, idList.size()); - } + final int maxPartitionSize = maxInBinding == 0 ? 1000 : maxInBinding; + final List> idLists = Lists.partition(maxPartitionSize, idList); + for (List ids : idLists) { + rows += deleteBatch(descriptor, id, ids, transaction, deleteMode); } - rows += deleteBatch(descriptor, id, idList, transaction, deleteMode); return rows; }