From bffc0fcd839a2ec167daadf4f3dd63aa8d0ff3f9 Mon Sep 17 00:00:00 2001 From: Alexander Lavrukov Date: Wed, 31 Jan 2024 14:23:43 +0300 Subject: [PATCH] split-table: split Table interface by behavior --- .../db/AbstractDelegatingTable.java | 6 + .../tech/ydb/yoj/repository/db/Table.java | 347 +----------------- .../yoj/repository/db/TableQueryBuilder.java | 5 +- .../yoj/repository/db/table/BaseTable.java | 10 + .../yoj/repository/db/table/ReadTable.java | 230 ++++++++++++ .../yoj/repository/db/table/TableReader.java | 23 ++ .../repository/db/table/UnsafeWriteTable.java | 37 ++ .../yoj/repository/db/table/WriteTable.java | 69 ++++ 8 files changed, 382 insertions(+), 345 deletions(-) create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/table/BaseTable.java create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/table/ReadTable.java create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/table/TableReader.java create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/table/UnsafeWriteTable.java create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/table/WriteTable.java diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java index 732ed724..82ab4c1b 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/AbstractDelegatingTable.java @@ -6,6 +6,7 @@ import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderExpression; import tech.ydb.yoj.repository.BaseDb; +import tech.ydb.yoj.repository.db.cache.FirstLevelCache; import tech.ydb.yoj.repository.db.readtable.ReadTableParams; import tech.ydb.yoj.repository.db.statement.Changeset; @@ -32,6 +33,11 @@ private Class resolveEntityType() { }).getRawType(); } + @Override + public FirstLevelCache getFirstLevelCache() { + return target.getFirstLevelCache(); + } + @Override public List find(@Nullable String indexName, @Nullable FilterExpression filter, @Nullable OrderExpression orderBy, @Nullable Integer limit, @Nullable Long offset) { return target.find(indexName, filter, orderBy, limit, offset); diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java b/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java index a9a5548f..cf41b678 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/Table.java @@ -1,355 +1,16 @@ package tech.ydb.yoj.repository.db; -import com.google.common.collect.Sets; -import lombok.NonNull; -import tech.ydb.yoj.databind.expression.FilterExpression; -import tech.ydb.yoj.databind.expression.OrderExpression; import tech.ydb.yoj.repository.db.bulk.BulkParams; -import tech.ydb.yoj.repository.db.cache.FirstLevelCache; -import tech.ydb.yoj.repository.db.list.ListRequest; -import tech.ydb.yoj.repository.db.list.ListResult; -import tech.ydb.yoj.repository.db.list.ViewListResult; -import tech.ydb.yoj.repository.db.readtable.ReadTableParams; -import tech.ydb.yoj.repository.db.statement.Changeset; +import tech.ydb.yoj.repository.db.table.TableReader; +import tech.ydb.yoj.repository.db.table.WriteTable; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static java.util.stream.Stream.concat; - -public interface Table> { - > Stream readTable(ReadTableParams params); - - > Stream readTableIds(ReadTableParams params); - - , ID extends Entity.Id> Stream readTable(Class viewClass, ReadTableParams params); - - Class getType(); - - @CheckForNull - T find(Entity.Id id); - - V find(Class viewType, Entity.Id id); - - > List find(Range range); - - > List findIds(Range range); - - > List findIds(Set partialIds); - - > List find(Class viewType, Range range); - - > List find(Class viewType, Set ids); - - List findAll(); - - List findAll(Class viewType); - - List find( - @Nullable String indexName, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit, - @Nullable Long offset - ); - - > List findIds( - @Nullable String indexName, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit, - @Nullable Long offset - ); - - List find( - Class viewType, - @Nullable String indexName, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit, - @Nullable Long offset, - boolean distinct - ); - - > List find( - Set ids, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit - ); - - > List findUncached( - Set ids, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit - ); - - > List find( - Class viewType, - Set ids, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit - ); - - List find( - String indexName, - Set keys, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit - ); - - List find( - Class viewType, - String indexName, - Set keys, - @Nullable FilterExpression filter, - @Nullable OrderExpression orderBy, - @Nullable Integer limit - ); - - Stream streamAll(int batchSize); - - > Stream streamAll(Class viewType, int batchSize); - - > Stream streamPartial(ID partial, int batchSize); - - , V extends ViewId> Stream streamPartial(Class viewType, ID partial, int batchSize); - - > Stream streamAllIds(int batchSize); - - > Stream streamPartialIds(ID partial, int batchSize); - - long count(String indexName, FilterExpression filter); - - long countAll(); - - // Unsafe - T insert(T t); - - // Unsafe - T save(T t); - - // Unsafe: may delete only entity, but not its projections, if entity was not loaded - void delete(Entity.Id id); - - // Unsafe - void deleteAll(); - - default Stream readTable() { - return readTable(ReadTableParams.getDefault()); - } - - default > Stream readTableIds() { - return readTableIds(ReadTableParams.getDefault()); - } - - default FirstLevelCache getFirstLevelCache() { - return null; - }; - - @NonNull - default T find(Entity.Id id, Supplier throwIfAbsent) throws X { - T found = find(id); - if (found != null) { - return found; - } else { - throw throwIfAbsent.get(); - } - } - - default T findOrDefault(Entity.Id id, Supplier defaultSupplier) { - T found = find(id); - return found != null ? found : defaultSupplier.get(); - } - - default V find(Class viewType, Entity.Id id, Supplier throwIfAbsent) throws X { - V found = find(viewType, id); - if (found != null) { - return found; - } else { - throw throwIfAbsent.get(); - } - } - - default T modifyIfPresent(Entity.Id id, Function modify) { - return Optional.ofNullable(find(id)) - .map(modify) - .map(this::save) - .orElse(null); - } - - default T generateAndSaveNew(@NonNull Supplier generator) { - for (int i = 0; i < 7; i++) { - T t = generator.get(); - if (find(t.getId()) == null) { - return save(t); - } - } - throw new IllegalStateException("Cannot generate unique entity id"); - } - - default T saveNewOrThrow(@NonNull T t, @NonNull Supplier alreadyExists) throws X { - if (find(t.getId()) != null) { - throw alreadyExists.get(); - } - return save(t); - } - - default T updateExistingOrThrow(@NonNull T t, @NonNull Supplier notFound) throws X { - if (find(t.getId()) == null) { - throw notFound.get(); - } - return save(t); - } - - default T saveOrUpdate(@NonNull T t) { - find(t.getId()); - return save(t); - } - - default T deleteIfExists(@NonNull Entity.Id id) { - T t = find(id); - if (t != null) { - delete(id); - } - return t; - } - - default > void deleteAll(Set ids) { - find(ids); - ids.forEach(this::delete); - } - - default > void deleteAll(Range range) { - find(range).forEach(e -> delete(e.getId())); - } - - // Unsafe - @SuppressWarnings("unchecked") - default void insert(T first, T... rest) { - insertAll(concat(Stream.of(first), Stream.of(rest)).collect(toList())); - } - - // Unsafe - default void insertAll(Collection entities) { - entities.forEach(this::insert); - } - - // Unsafe - default > void delete(Set ids) { - ids.forEach(this::delete); - } - - // Unsafe - default > void delete(Range range) { - findIds(range).forEach(this::delete); - } - - default ListResult list(ListRequest request) { - List nextPage = toQueryBuilder(request).find(); - return ListResult.forPage(request, postLoad(nextPage)); - } - - default ViewListResult list(Class viewType, ListRequest request) { - List nextPage = toQueryBuilder(request).find(viewType); - return ViewListResult.forPage(request, viewType, nextPage); - } - - default > List find(Set ids) { - if (ids.isEmpty()) { - return List.of(); - } - - var orderBy = EntityExpressions.defaultOrder(getType()); - var cache = getFirstLevelCache(); - var isPartialIdMode = ids.iterator().next().isPartial(); - - var foundInCache = ids.stream() - .filter(cache::containsKey) - .map(cache::peek) - .flatMap(Optional::stream) - .collect(Collectors.toMap(Entity::getId, Function.identity())); - var remainingIds = Sets.difference(ids, foundInCache.keySet()); - var foundInDb = findUncached(remainingIds, null, orderBy, null); - - var merged = new HashMap, T>(); - - // some entries found in db with partial id query may already be in cache (after update/delete), - // so we must return actual entries from cache - for (var entry : foundInDb) { - var id = entry.getId(); - if (cache.containsKey(id)) { - var cached = cache.peek(id); - cached.ifPresent(t -> merged.put(id, t)); - // not present means marked as deleted in cache - } else { - merged.put(id, this.postLoad(entry)); - } - } - - // add entries found in cache and not fetched from db - for (var pair : foundInCache.entrySet()) { - var id = pair.getKey(); - var entry = pair.getValue(); - merged.put(id, entry); - } - - if (!isPartialIdMode) { - Set> foundInDbIds = foundInDb.stream().map(Entity::getId).collect(toSet()); - Set> foundInCacheIds = new HashSet<>(foundInCache.keySet()); - Sets.difference(Sets.difference(ids, foundInDbIds), foundInCacheIds).forEach(cache::putEmpty); - } - - return merged.values().stream().sorted(EntityIdSchema.SORT_ENTITY_BY_ID).collect(Collectors.toList()); - } +public interface Table> extends TableReader, WriteTable { default void bulkUpsert(List input, BulkParams params) { throw new UnsupportedOperationException(); } - default TableQueryBuilder toQueryBuilder(ListRequest request) { - return query() - .index(request.getIndex()) - .filter(request.getFilter()) - .orderBy(request.getOrderBy()) - .offset(request.getOffset()) - .limit(request.getPageSize() + 1); - } - - default List postLoad(List list) { - return list.stream().map(this::postLoad).collect(Collectors.toList()); - } - - default T postLoad(T e) { - return e.postLoad(); - } - - default long count(FilterExpression filter) { - return count(null, filter); - } - - default TableQueryBuilder query() { - return new TableQueryBuilder<>(this); - } - - @Deprecated - void update(Entity.Id id, Changeset changeset); - interface View { } @@ -361,7 +22,7 @@ interface ViewId> extends View { * Base interface for ID-aware table views that are Java {@link java.lang.Record records}. *

Forwards {@link ViewId#getId() ViewId's getId() method} to the record's {@code id()} accessor. * - * @param entity type + * @param entity type */ interface RecordViewId> extends ViewId { Entity.Id id(); diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java index e87d6695..59c75a57 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java @@ -7,6 +7,7 @@ import tech.ydb.yoj.databind.expression.FilterExpression; import tech.ydb.yoj.databind.expression.OrderBuilder; import tech.ydb.yoj.databind.expression.OrderExpression; +import tech.ydb.yoj.repository.db.table.ReadTable; import javax.annotation.Nullable; import java.util.Collection; @@ -17,7 +18,7 @@ import static lombok.AccessLevel.PRIVATE; public final class TableQueryBuilder> { - private final Table table; + private final ReadTable table; private Set> ids; private Set keys; @@ -31,7 +32,7 @@ public final class TableQueryBuilder> { private OrderExpression orderBy = null; - public TableQueryBuilder(@NonNull Table table) { + public TableQueryBuilder(@NonNull ReadTable table) { this.table = table; } diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/table/BaseTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/table/BaseTable.java new file mode 100644 index 00000000..6d7e8f9b --- /dev/null +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/table/BaseTable.java @@ -0,0 +1,10 @@ +package tech.ydb.yoj.repository.db.table; + +import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.cache.FirstLevelCache; + +public interface BaseTable> { + Class getType(); + + FirstLevelCache getFirstLevelCache(); +} diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/table/ReadTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/table/ReadTable.java new file mode 100644 index 00000000..a93887d6 --- /dev/null +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/table/ReadTable.java @@ -0,0 +1,230 @@ +package tech.ydb.yoj.repository.db.table; + +import com.google.common.collect.Sets; +import lombok.NonNull; +import tech.ydb.yoj.databind.expression.FilterExpression; +import tech.ydb.yoj.databind.expression.OrderExpression; +import tech.ydb.yoj.repository.db.*; +import tech.ydb.yoj.repository.db.list.ListRequest; +import tech.ydb.yoj.repository.db.list.ListResult; +import tech.ydb.yoj.repository.db.list.ViewListResult; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; + +public interface ReadTable> extends BaseTable { + + @CheckForNull + T find(Entity.Id id); + + V find(Class viewType, Entity.Id id); + + > List find(Range range); + + > List findIds(Range range); + + > List findIds(Set partialIds); + + > List find(Class viewType, Range range); + + > List find(Class viewType, Set ids); + + List findAll(); + + List findAll(Class viewType); + + List find( + @Nullable String indexName, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit, + @Nullable Long offset + ); + + > List findIds( + @Nullable String indexName, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit, + @Nullable Long offset + ); + + List find( + Class viewType, + @Nullable String indexName, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit, + @Nullable Long offset, + boolean distinct + ); + + > List find( + Set ids, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit + ); + + > List findUncached( + Set ids, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit + ); + + > List find( + Class viewType, + Set ids, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit + ); + + List find( + String indexName, + Set keys, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit + ); + + List find( + Class viewType, + String indexName, + Set keys, + @Nullable FilterExpression filter, + @Nullable OrderExpression orderBy, + @Nullable Integer limit + ); + + Stream streamAll(int batchSize); + + > Stream streamAll(Class viewType, int batchSize); + + > Stream streamPartial(ID partial, int batchSize); + + , V extends Table.ViewId> Stream streamPartial(Class viewType, ID partial, int batchSize); + + > Stream streamAllIds(int batchSize); + + > Stream streamPartialIds(ID partial, int batchSize); + + long count(String indexName, FilterExpression filter); + + long countAll(); + + @NonNull + default T find(Entity.Id id, Supplier throwIfAbsent) throws X { + T found = find(id); + if (found != null) { + return found; + } else { + throw throwIfAbsent.get(); + } + } + + default T findOrDefault(Entity.Id id, Supplier defaultSupplier) { + T found = find(id); + return found != null ? found : defaultSupplier.get(); + } + + default V find(Class viewType, Entity.Id id, Supplier throwIfAbsent) throws X { + V found = find(viewType, id); + if (found != null) { + return found; + } else { + throw throwIfAbsent.get(); + } + } + + default ListResult list(ListRequest request) { + List nextPage = toQueryBuilder(request).find(); + return ListResult.forPage(request, postLoad(nextPage)); + } + + default ViewListResult list(Class viewType, ListRequest request) { + List nextPage = toQueryBuilder(request).find(viewType); + return ViewListResult.forPage(request, viewType, nextPage); + } + + default > List find(Set ids) { + if (ids.isEmpty()) { + return List.of(); + } + + var orderBy = EntityExpressions.defaultOrder(getType()); + var cache = getFirstLevelCache(); + var isPartialIdMode = ids.iterator().next().isPartial(); + + var foundInCache = ids.stream() + .filter(cache::containsKey) + .map(cache::peek) + .flatMap(Optional::stream) + .collect(Collectors.toMap(Entity::getId, Function.identity())); + var remainingIds = Sets.difference(ids, foundInCache.keySet()); + var foundInDb = findUncached(remainingIds, null, orderBy, null); + + var merged = new HashMap, T>(); + + // some entries found in db with partial id query may already be in cache (after update/delete), + // so we must return actual entries from cache + for (var entry : foundInDb) { + var id = entry.getId(); + if (cache.containsKey(id)) { + var cached = cache.peek(id); + cached.ifPresent(t -> merged.put(id, t)); + // not present means marked as deleted in cache + } else { + merged.put(id, this.postLoad(entry)); + } + } + + // add entries found in cache and not fetched from db + for (var pair : foundInCache.entrySet()) { + var id = pair.getKey(); + var entry = pair.getValue(); + merged.put(id, entry); + } + + if (!isPartialIdMode) { + Set> foundInDbIds = foundInDb.stream().map(Entity::getId).collect(toSet()); + Set> foundInCacheIds = new HashSet<>(foundInCache.keySet()); + Sets.difference(Sets.difference(ids, foundInDbIds), foundInCacheIds).forEach(cache::putEmpty); + } + + return merged.values().stream().sorted(EntityIdSchema.SORT_ENTITY_BY_ID).collect(Collectors.toList()); + } + + default TableQueryBuilder toQueryBuilder(ListRequest request) { + return query() + .index(request.getIndex()) + .filter(request.getFilter()) + .orderBy(request.getOrderBy()) + .offset(request.getOffset()) + .limit(request.getPageSize() + 1); + } + + default List postLoad(List list) { + return list.stream().map(this::postLoad).collect(Collectors.toList()); + } + + default T postLoad(T e) { + return e.postLoad(); + } + + default long count(FilterExpression filter) { + return count(null, filter); + } + + default TableQueryBuilder query() { + return new TableQueryBuilder<>(this); + } +} diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/table/TableReader.java b/repository/src/main/java/tech/ydb/yoj/repository/db/table/TableReader.java new file mode 100644 index 00000000..678a295d --- /dev/null +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/table/TableReader.java @@ -0,0 +1,23 @@ +package tech.ydb.yoj.repository.db.table; + +import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.Table; +import tech.ydb.yoj.repository.db.readtable.ReadTableParams; + +import java.util.stream.Stream; + +public interface TableReader> { + > Stream readTable(ReadTableParams params); + + > Stream readTableIds(ReadTableParams params); + + , ID extends Entity.Id> Stream readTable(Class viewClass, ReadTableParams params); + + default Stream readTable() { + return readTable(ReadTableParams.getDefault()); + } + + default > Stream readTableIds() { + return readTableIds(ReadTableParams.getDefault()); + } +} diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/table/UnsafeWriteTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/table/UnsafeWriteTable.java new file mode 100644 index 00000000..1ab37aa3 --- /dev/null +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/table/UnsafeWriteTable.java @@ -0,0 +1,37 @@ +package tech.ydb.yoj.repository.db.table; + +import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.statement.Changeset; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Stream.concat; + +public interface UnsafeWriteTable> extends BaseTable { + T insert(T t); + + T save(T t); + + void delete(Entity.Id id); + + void deleteAll(); + + @Deprecated + void update(Entity.Id id, Changeset changeset); + + @SuppressWarnings("unchecked") + default void insert(T first, T... rest) { + insertAll(concat(Stream.of(first), Stream.of(rest)).collect(toList())); + } + + default void insertAll(Collection entities) { + entities.forEach(this::insert); + } + + default > void delete(Set ids) { + ids.forEach(this::delete); + } +} diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/table/WriteTable.java b/repository/src/main/java/tech/ydb/yoj/repository/db/table/WriteTable.java new file mode 100644 index 00000000..7347f981 --- /dev/null +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/table/WriteTable.java @@ -0,0 +1,69 @@ +package tech.ydb.yoj.repository.db.table; + +import lombok.NonNull; +import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.Range; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface WriteTable> extends UnsafeWriteTable, ReadTable { + default > void deleteAll(Range range) { + find(range).forEach(e -> delete(e.getId())); + } + + default T modifyIfPresent(Entity.Id id, Function modify) { + return Optional.ofNullable(find(id)) + .map(modify) + .map(this::save) + .orElse(null); + } + + default T generateAndSaveNew(@NonNull Supplier generator) { + for (int i = 0; i < 7; i++) { + T t = generator.get(); + if (find(t.getId()) == null) { + return save(t); + } + } + throw new IllegalStateException("Cannot generate unique entity id"); + } + + default T saveNewOrThrow(@NonNull T t, @NonNull Supplier alreadyExists) throws X { + if (find(t.getId()) != null) { + throw alreadyExists.get(); + } + return save(t); + } + + default T updateExistingOrThrow(@NonNull T t, @NonNull Supplier notFound) throws X { + if (find(t.getId()) == null) { + throw notFound.get(); + } + return save(t); + } + + default T saveOrUpdate(@NonNull T t) { + find(t.getId()); + return save(t); + } + + default T deleteIfExists(@NonNull Entity.Id id) { + T t = find(id); + if (t != null) { + delete(id); + } + return t; + } + + default > void deleteAll(Set ids) { + find(ids); + ids.forEach(this::delete); + } + + default > void delete(Range range) { + findIds(range).forEach(this::delete); + } +}