Автор оригинала: Vlad Mihalcea.
Вступление
В моем предыдущем посте я объяснил конфигурации гибернации , необходимые для пакетной обработки инструкций INSERT и UPDATE. В этом посте эта тема будет продолжена пакетной обработкой операторов УДАЛЕНИЯ.
Объекты модели домена
Мы начнем со следующей модели сущности:
Сообщение сущность имеет связь “один ко многим | с Комментарием/| и связь” один к одному ” с Деталями сообщения сущностью:
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private Listcomments = 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 AtomicReferencestartNanos = 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 AtomicReferencestartNanos = 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 Listcomments = 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 -> { Listposts = 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]}
Вывод
Если ВСТАВКА и ОБНОВЛЕНИЕ пакетирование операторов-это всего лишь вопрос конфигурации, УДАЛЕНИЕ операторов требует некоторых дополнительных шагов, которые могут увеличить сложность уровня доступа к данным.