Автор оригинала: Vlad Mihalcea.
Вступление
В моем предыдущем посте я объяснил конфигурации гибернации , необходимые для пакетной обработки инструкций INSERT и UPDATE. В этом посте эта тема будет продолжена пакетной обработкой операторов УДАЛЕНИЯ.
Объекты модели домена
Мы начнем со следующей модели сущности:
Сообщение сущность имеет связь “один ко многим | с Комментарием/| и связь” один к одному ” с Деталями сообщения сущностью:
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List comments = new ArrayList<>();
@OneToOne(
mappedBy = "post",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true
)
private PostDetails details;
Предстоящие тесты будут проводиться на основе следующих данных:
int batchSize = batchSize();
for(int i = 0; i < itemsCount(); i++) {
int j = 0;
Post post = new Post(
String.format("Post no. %d", i)
);
post.addComment(
new Comment(
String.format("Post comment %d:%d", i, j++)
)
);
post.addComment(
new Comment(
String.format("Post comment %d:%d", i, j++)
)
);
post.addDetails(new PostDetails());
session.persist(post);
if(i % batchSize == 0 && i > 0) {
session.flush();
session.clear();
}
}
Конфигурация спящего режима
Как уже объяснялось , для пакетной обработки инструкций INSERT и UPDATE требуются следующие свойства:
Далее мы проверим, являются ли операторы УДАЛЕНИЯ также пакетными.
Каскадное удаление JPA
Поскольку каскадные переходы состояний сущностей удобны, я собираюсь доказать, что Каскадный тип.УДАЛЕНИЕ и JDBC пакетирование плохо сочетаются.
Следующие тесты будут:
- Выберите некоторые Сообщения вместе с Комментариями и Деталями публикации
- Удалите Сообщения , одновременно распространяя событие удаления на Комментарии и Сведения о публикации , а также
LOGGER.info("Test batch delete with cascade");
final AtomicReference startNanos = new AtomicReference<>();
addDeleteBatchingRows();
doInTransaction(session -> {
List posts = session.createQuery(
"select distinct p " +
"from Post p " +
"join fetch p.details d " +
"join fetch p.comments c")
.list();
startNanos.set(System.nanoTime());
for (Post post : posts) {
session.delete(post);
}
});
LOGGER.info("{}.testCascadeDelete took {} millis",
getClass().getSimpleName(),
TimeUnit.NANOSECONDS.toMillis(
System.nanoTime() - startNanos.get()
));
Выполнение этого теста дает следующий результат:
Query:{[delete from Comment where id=? and version=?][55,0]}
{[delete from Comment where id=? and version=?][56,0]}
Query:{[delete from PostDetails where id=?][3]}
Query:{[delete from Post where id=? and version=?][3,0]}
Query:{[delete from Comment where id=? and version=?][54,0]}
{[delete from Comment where id=? and version=?][53,0]}
Query:{[delete from PostDetails where id=?][2]}
Query:{[delete from Post where id=? and version=?][2,0]}
Query:{[delete from Comment where id=? and version=?][52,0]}
{[delete from Comment where id=? and version=?][51,0]}
Query:{[delete from PostDetails where id=?][1]}
Query:{[delete from Post where id=? and version=?][1,0]}
Были сгруппированы только операторы Comment DELETE, остальные объекты удалялись в отдельных циклах базы данных.
Причина такого поведения объясняется реализацией ActionQueue сортировки:
if ( session.getFactory().getSettings().isOrderUpdatesEnabled() ) {
// sort the updates by pk
updates.sort();
}
if ( session.getFactory().getSettings().isOrderInsertsEnabled() ) {
insertions.sort();
}
В то время как ВСТАВКИ и ОБНОВЛЕНИЯ охвачены, УДАЛЕНИЕ операторы вообще не сортируются. Пакет JDBC можно использовать повторно только в том случае, если все операторы принадлежат одной и той же таблице базы данных. Когда входящий оператор нацелен на другую таблицу базы данных, текущий пакет должен быть выпущен, чтобы новый пакет соответствовал текущей таблице базы данных оператора:
public Batch getBatch(BatchKey key) {
if ( currentBatch != null ) {
if ( currentBatch.getKey().equals( key ) ) {
return currentBatch;
}
else {
currentBatch.execute();
currentBatch.release();
}
}
currentBatch = batchBuilder().buildBatch(key, this);
return currentBatch;
}
Удаление сирот и ручная промывка
Обходной путь состоит в том, чтобы разъединить все Дочерние сущности при ручной очистке Гибернации Сеанса перед переходом к новой Дочерней ассоциации:
LOGGER.info("Test batch delete with orphan removal");
final AtomicReference startNanos = new AtomicReference<>();
addDeleteBatchingRows();
doInTransaction(session -> {
List posts = session.createQuery(
"select distinct p " +
"from Post p " +
"join fetch p.details d " +
"join fetch p.comments c")
.list();
startNanos.set(System.nanoTime());
posts.forEach(Post::removeDetails);
session.flush();
posts.forEach(post -> {
for (Iterator commentIterator =
post.getComments().iterator(); commentIterator.hasNext(); ) {
Comment comment = commentIterator.next();
comment.post = null;
commentIterator.remove();
}
});
session.flush();
posts.forEach(session::delete);
});
LOGGER.info("{}.testOrphanRemoval took {} millis",
getClass().getSimpleName(),
TimeUnit.NANOSECONDS.toMillis(
System.nanoTime() - startNanos.get()
));
На этот раз все УДАЛИТЬ операторы правильно сопоставлены:
Query:{[delete from PostDetails where id=?][2]}
{[delete from PostDetails where id=?][3]}
{[delete from PostDetails where id=?][1]}
Query:{[delete from Comment where id=? and version=?][53,0]}
{[delete from Comment where id=? and version=?][54,0]}
{[delete from Comment where id=? and version=?][56,0]}
{[delete from Comment where id=? and version=?][55,0]}
{[delete from Comment where id=? and version=?][52,0]}
{[delete from Comment where id=? and version=?][51,0]}
Query:{[delete from Post where id=? and version=?][2,0]}
{[delete from Post where id=? and version=?][3,0]}
{[delete from Post where id=? and version=?][1,0]}
Каскадное удаление SQL
Лучшим решением является использование SQL каскадного удаления вместо JPA механизма распространения состояния сущности. Таким образом, мы также можем уменьшить количество операторов DML . Поскольку сеанс гибернации действует как транзакционный кэш с последующей записью , мы должны быть особенно осторожны при смешивании переходов состояний сущностей с автоматическими действиями на стороне базы данных, так как контекст Сохранения может не отражать последние изменения базы данных.
Сообщение сущность один ко многим | Комментарий ассоциация помечена аннотацией Hibernate specific @OnDelete , так что автоматически сгенерированная схема базы данных включает директиву ПРИ УДАЛЕНИИ КАСКАДА :
@OneToMany(
mappedBy = "post"
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
)
@OnDelete(action = OnDeleteAction.CASCADE)
private List comments = new ArrayList<>();
Создание следующего DDL :
ALTER TABLE Comment ADD CONSTRAINT FK_COMMENT_POST_ID FOREIGN KEY (post_id) REFERENCES Post ON DELETE CASCADE
То же самое делается с Деталями записи сущностью один к одному Сообщением ассоциацией:
@OneToOne(fetch = FetchType.LAZY) @MapsId @OnDelete(action = OnDeleteAction.CASCADE) private Post post;
И связанный с этим DDL :
ALTER TABLE PostDetails ADD CONSTRAINT FK_POST_DETAILS_ID
Тип каскада .ВСЕ и orphanRemoval были заменены на Каскадный тип.СОХРАНЯЙТЕ и Каскадный тип.СЛИЯНИЕ , потому что мы больше не хотим, чтобы Hibernate распространял событие удаления сущности.
Тест удаляет только объекты записи.
doInTransaction(session -> {
List posts = session.createQuery(
"select p from Post p")
.list();
startNanos.set(System.nanoTime());
for (Post post : posts) {
session.delete(post);
}
});
Операторы DELETE правильно сгруппированы, так как существует только одна целевая таблица.
Query:{[delete from Post where id=? and version=?][1,0]}
{[delete from Post where id=? and version=?][2,0]}
{[delete from Post where id=? and version=?][3,0]}
Вывод
Если ВСТАВКА и ОБНОВЛЕНИЕ пакетирование операторов-это всего лишь вопрос конфигурации, УДАЛЕНИЕ операторов требует некоторых дополнительных шагов, которые могут увеличить сложность уровня доступа к данным.