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

Массовое обновление оптимистичной блокировки с помощью JPA и гибернации

Узнайте, как использовать оптимистическую блокировку при выполнении запроса массового обновления через JPQL Hibernate или при использовании API критериев JPA.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье мы рассмотрим, как мы можем настроить оператор массового обновления таким образом, чтобы он учитывал оптимистическую блокировку.

В то время как Hibernate поддерживает версионные запросы HQL в течение очень долгого времени, на самом деле очень легко достичь этой цели даже со стандартным API критериев JPQL или JPA.

Массовое обновление оптимистичной блокировки с помощью JPA и гибернации. https://t.co/OsBnhkiHQj

Модель предметной области

Давайте рассмотрим, что у нас есть Сообщение сущность, имеющая статус атрибут, который может принимать три возможных значения: В ОЖИДАНИИ , ОДОБРЕНО и СПАМ . По этой причине атрибут статус сопоставляется с типом перечисления Статус публикации .

Для получения более подробной информации о том, как лучше всего сопоставить тип перечисления при использовании JPA и гибернации, ознакомьтесь с этой статьей .

Объект Post отображается следующим образом:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    @Enumerated(EnumType.ORDINAL)
    @Column(columnDefinition = "tinyint")
    private PostStatus status = PostStatus.PENDING;

    @Version
    private short version;

    public Long getId() {
        return id;
    }

    public Post setId(Long id) {
        this.id = id;
        return this;
    }

    public String getTitle() {
        return title;
    }

    public Post setTitle(String title) {
        this.title = title;
        return this;
    }

    public PostStatus getStatus() {
        return status;
    }

    public void setStatus(PostStatus status) {
        this.status = status;
    }

    public short getVersion() {
        return version;
    }

    public Post setVersion(short version) {
        this.version = version;
        return this;
    }
}

Если вам интересно, почему свойство version сопоставлено с коротким Тип примитива Java, вместо int или long , тогда эта статья объяснит вам, почему короткая чаще всего является гораздо лучшей альтернативой.

Теперь давайте предположим, что у нас есть несколько Сообщений объекты, которые должны быть помечены как спам`:

for (long i = 1; i <= SPAM_POST_COUNT; i++) {
    entityManager.persist(
        new Post()
            .setId(i)
            .setTitle(String.format("Spam post %d", i))
    );
}

Обратите внимание, что методы Java setter возвращают текущую ссылку Post на объект, что позволяет нам создать Опубликуйте экземпляр сущности API в стиле Fluent. Для получения более подробной информации об этой теме ознакомьтесь с этой статьей .

Массовое обновление

Теперь, используя обычный JPQL, оператор массового обновления будет выглядеть следующим образом:

int updateCount = entityManager.createQuery("""
    update Post
    set status = :newStatus
    where
        status = :oldStatus and
        lower(title) like :pattern
    """)
.setParameter("oldStatus", PostStatus.PENDING)
.setParameter("newStatus", PostStatus.SPAM)
.setParameter("pattern", "%spam%")
.executeUpdate();

assertEquals(SPAM_POST_COUNT, updateCount);

При выполнении приведенной выше инструкции JPQL Hibernate генерирует следующую инструкцию SQL:

UPDATE post
SET 
  status = 2
WHERE 
  status = 0 AND  
  lower(title) LIKE '%spam%'

Однако столбец версия просто игнорируется, и, если есть незавершенные транзакции, которые уже загрузили объект Post , они потеряют это обновление, если мы не увеличим свойство версии, как показано на диаграмме ниже.

На приведенной выше диаграмме Алиса выбирает Должность юридическое лицо. После этого Боб запускает массовое обновление, изменяющее состояние всех записей post , содержащих слово спам . Если последующее ОБНОВЛЕНИЕ Алисы будет разрешено запустить, она никогда не признает изменения Боба. Вот как возникает аномалия потерянного обновления.

Если вы хотите узнать больше об аномалии потерянного обновления и о том, как ее лучше предотвратить, ознакомьтесь с этой статьей .

Массовое обновление с оптимистичной блокировкой

Теперь избежать потерянного обновления на самом деле очень просто с JPQL, так как все, что нам нужно сделать, это увеличить атрибут версия сущность:

int updateCount = entityManager.createQuery("""
    update Post
    set
        status = :newStatus,
        version = version + 1
    where
        status = :oldStatus and
        lower(title) like :pattern
    """)
.setParameter("oldStatus", PostStatus.PENDING)
.setParameter("newStatus", PostStatus.SPAM)
.setParameter("pattern", "%spam%")
.executeUpdate();

assertEquals(SPAM_POST_COUNT, updateCount);

Теперь при выполнении приведенного выше запроса JPQL Hibernate генерирует следующую инструкцию ОБНОВЛЕНИЯ SQL:

UPDATE post
SET 
  status = 2,
  version = version + 1
WHERE 
  status = 0 AND  
  lower(title) LIKE '%spam%'

При увеличении столбца версия ОБНОВЛЕНИЕ Алисы не будет успешным, так как предложение WHERE не будет соответствовать записи post и На этот раз будет выдано исключение OptimisticLockException .

Массовое обновление версии гибернации

Hibernate поддерживает упрощенную версию предыдущей инструкции массового обновления JPQL, которая увеличила столбец версии. Вместо увеличения столбца версии вам просто нужно указать ключевое слово с версией после обновления .

int updateCount = entityManager.createQuery("""
    update versioned Post
    set status = :newStatus
    where
        status = :oldStatus and
        lower(title) like :pattern
    """)
.setParameter("oldStatus", PostStatus.PENDING)
.setParameter("newStatus", PostStatus.SPAM)
.setParameter("pattern", "%spam%")
.executeUpdate();

assertEquals(SPAM_POST_COUNT, updateCount);

Критерии массовое обновление API оптимистичная блокировка

Обновление столбца версия в инструкции массового обновления не ограничивается JPQL или HQL. Если вы хотите динамически создавать оператор массового обновления, то API критериев является гораздо лучшей альтернативой, чем объединение фрагментов строк запроса, что может привести к атакам SQL-инъекций .

Предыдущая инструкция массового обновления JPQL, которая увеличивала атрибут версия , может быть переведена в API критериев следующим образом:

CriteriaBuilder builder = entityManager
.getCriteriaBuilder();

CriteriaUpdate update = builder
.createCriteriaUpdate(Post.class);

Root root = update.from(Post.class);

Expression wherePredicate = builder
.and(
    builder.equal(
        root.get("status"), 
        PostStatus.PENDING
    ),
    builder.like(
        builder.lower(root.get("title")), 
        "%spam%"
    )
);

Path versionPath = root.get("version");
Expression incrementVersion = builder
.sum((short) 1, versionPath);

update
.set(root.get("status"), PostStatus.SPAM)
.set(versionPath, incrementVersion)
.where(wherePredicate);

int updateCount = entityManager
.createQuery(update)
.executeUpdate();

Вывод

Если вы используете оптимистичную стратегию блокировки для предотвращения аномалий потерянных обновлений, то Hibernate может автоматически увеличивать столбец версия при каждом обновлении сущности. Однако для операторов массового обновления это не так, и вам необходимо настроить операторы массового обновления таким образом, чтобы столбец версия соответственно увеличивался.