Рубрики
Без рубрики

Как пакетно УДАЛЯТЬ инструкции с помощью Hibernate

Узнайте, как работает пакетное удаление JDBC при использовании JPA и Hibernate, и мы используем либо каскадирование объектов JPA, либо DDL ПРИ КАСКАДНОМ УДАЛЕНИИ.

Автор оригинала: 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]}

Вывод

Если ВСТАВКА и ОБНОВЛЕНИЕ пакетирование операторов-это всего лишь вопрос конфигурации, УДАЛЕНИЕ операторов требует некоторых дополнительных шагов, которые могут увеличить сложность уровня доступа к данным.