Предотвращение параллелизма в базе данных с помощью @Version JPA и Hibernate

Когда вы работаете над проектом крупной бизнес-компании, иногда кто-то спрашивает: «Что будет, если два пользователя обновят одну и ту же запись в базе данных?».

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

В Java Persistence API (JPA) есть аннотация @version, которая поможет вам проверить, сколько раз обновлялась запись в базе данных.

Давайте посмотрим на эти простые таблицы и сущности.

create table device
(
    id      integer not null constraint device_pk primary key,
    serial  integer,
    name    varchar(255),
    version integer
)

Вход в полноэкранный режим Выход из полноэкранного режима

И сущность

package com.vominh.example.entity;

import javax.persistence.*;

@Entity
@Table(name = "device")
public class DeviceWithVersionEntity {
    @Id
    @Column(name = "id")
    private Integer id;
    @Column(name = "serial")
    private Integer serial;
    @Column(name = "name")
    private String name;
    @Version
    @Column(name = "version")
    private int version;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getSerial() {
        return serial;
    }
    public void setSerial(Integer serial) {
        this.serial = serial;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Часть может быть понятна как «Это поле увеличится на 1, когда кто-то выполнит над ним действие обновления».

@Version
@Column(name = "version")
private int version;
Вход в полноэкранный режим Выход из полноэкранного режима

Главный класс

package com.vominh.example;

import com.vominh.example.entity.DeviceWithVersionEntity;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;

import java.util.Random;
import java.util.concurrent.CompletableFuture;

public class ConcurrencyControl {
    private static final SessionFactory sessionFactory;
    private static final ServiceRegistry serviceRegistry;

    static {
        Configuration configuration = new Configuration();
        configuration.configure("hibernate.cfg.xml");

        // Since Hibernate Version 4.x, ServiceRegistry Is Being Used
        serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build();
        sessionFactory = configuration.buildSessionFactory(serviceRegistry);
    }

    public static void main(String[] args) {
        Session session = sessionFactory.openSession();
        Query deleteAllDevice = session.createQuery("delete from DeviceWithVersionEntity");

        try {
            session.beginTransaction();
            deleteAllDevice.executeUpdate();

            DeviceWithVersionEntity device = new DeviceWithVersionEntity();
            device.setId(1);
            device.setSerial(8888);
            device.setName("Dell xps 99");

            session.save(device);

            session.getTransaction().commit();
        } catch (Exception e) {
            e.printStackTrace();
            session.getTransaction().rollback();
        } finally {
            session.close();
        }

        // open 50 session in 50 thread to update one record
        for (int i = 0; i < 50; i++) {
            CompletableFuture.runAsync(() -> {
                var s = sessionFactory.openSession();
                try {
                    s.beginTransaction();

                    DeviceWithVersionEntity d = (DeviceWithVersionEntity) s.load(DeviceWithVersionEntity.class, 1);
                    d.setName((new Random()).nextInt(500) + "");
                    s.save(d);

                    s.getTransaction().commit();
                } catch (Exception e) {
                    e.printStackTrace();
                    s.getTransaction().rollback();
                } finally {
                    s.close();
                }
            });
        }
    }
}

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

В результате выполнения будет выброшено множество org.hibernate.StaleObjectStateException


Сообщение об исключении уже объясняет, что происходит, но как и когда?

Строка была обновлена или удалена другой транзакцией (или несохраненное отображение значений было неправильным)

Если @version добавить к полю сущности (число), то при выполнении операции создания/обновления Hibernate будет устанавливать/увеличивать значение поля @version.

UPDATE device set name = 'new name', version = version + 1
Вход в полноэкранный режим Выход из полноэкранного режима

Когда произойдет следующее UPDATE, Hibernate проверит, совпадает ли версия, добавив условие в предложение WHERE.

UPDATE device SET name = 'new name', version = version + 1
WHERE id = ? AND version = **CURRENT_VERSION**
Войти в полноэкранный режим Выйти из полноэкранного режима

если другая сессия уже обновила запись и версия увеличилась, условие WHERE не будет соответствовать и будет выброшено исключение.

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

Исходный код доступен по адресу: https://github.com/docvominh/java-example/tree/master/hibernate-jpa
Я использую maven, hibernate 4.3 и postgres sql.

Спасибо за прочтение!

Оцените статью
Procodings.ru
Добавить комментарий