Статьи

Hibernate CascadeType.LOCK получил ошибки

Вступление

Введя поддержку явной блокировки Hibernate, а также Cascade Types , пришло время проанализировать поведение CascadeType.LOCK .

Запрос блокировки Hibernate запускает внутренний LockEvent . Связанный DefaultLockEventListener может каскадно запросить блокировку для дочерних объектов блокировки.

Поскольку CascadeType.ALL также включает CascadeType.LOCK , стоит понимать, когда запрос блокировки распространяется от родительского объекта к дочернему объекту.

Время тестирования

Начнем со следующей модели сущности:

postcommentdetailslockcascade

Post является родительской сущностью как связи один-к-одному PostDetail, так и отношения один-ко-многим Comment , и эти ассоциации помечаются CascadeType.ALL

01
02
03
04
05
06
07
08
09
10
11
12
@OneToMany(
    cascade = CascadeType.ALL,
    mappedBy = "post",
    orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
 
@OneToOne(
    cascade = CascadeType.ALL,
    mappedBy = "post",
    optional = false,
    fetch = FetchType.LAZY)
private PostDetails details;

Во всех будущих тестовых случаях будет использоваться следующий график модели сущностей:

01
02
03
04
05
06
07
08
09
10
doInTransaction(session -> {
    Post post = new Post();
    post.setName("Hibernate Master Class");
 
    post.addDetails(new PostDetails());
    post.addComment(new Comment("Good post!"));
    post.addComment(new Comment("Nice post!"));
 
    session.persist(post);
});

Блокировка управляемых объектов

Управляемый объект загружается в текущий текущий контекст сохраняемости, и все изменения состояния объекта переводятся в операторы DML .

Когда управляемый родительский объект заблокирован:

01
02
03
04
05
06
07
08
09
10
11
12
13
doInTransaction(session -> {
    Post post = (Post) session.createQuery(
        "select p " +
        "from Post p " +
        "join fetch p.details " +
        "where " +
        "   p.id = :id")
    .setParameter("id", 1L)
    .uniqueResult();
    session.buildLockRequest(
        new LockOptions(LockMode.PESSIMISTIC_WRITE))
    .lock(post);
});

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

1
select id from Post where id = 1 for update

Hibernate определяет область действия LockOption , которая (согласно JavaDocs) должна позволять распространять запрос блокировки на дочерние объекты:

«Область действия» — это термин, определенный JPA. Это в основном каскадирование блокировки ассоциаций.

1
2
3
4
session.buildLockRequest(
    new LockOptions(LockMode.PESSIMISTIC_WRITE))
.setScope(true)
.lock(post);

Установка флага области ничего не меняет, блокируется только управляемый объект:

1
select id from Post where id = 1 for update

Блокировка отдельных объектов

Помимо блокировки сущностей, запрос на блокировку может также повторно связать отдельные сущности. Чтобы доказать это, мы собираемся проверить граф сущности Post до и после запроса блокировки сущности:

01
02
03
04
05
06
07
08
09
10
11
void containsPost(Session session,
    Post post, boolean expected) {
    assertEquals(expected,
        session.contains(post));
    assertEquals(expected,
        session.contains(post.getDetails()));
    for(Comment comment : post.getComments()) {
        assertEquals(expected,
            session.contains(comment));
    }
}

Следующий тест демонстрирует, как CascadeType.LOCK работает для отдельных объектов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//Load the Post entity, which will become detached
Post post = doInTransaction(session ->
   (Post) session.createQuery(
        "select p " +
        "from Post p " +
        "join fetch p.details " +
        "join fetch p.comments " +
        "where " +
        "   p.id = :id")
.setParameter("id", 1L)
.uniqueResult());
 
//Change the detached entity state
post.setName("Hibernate Training");
doInTransaction(session -> {
    //The Post entity graph is detached
    containsPost(session, post, false);
 
    //The Lock request associates
    //the entity graph and locks the requested entity
    session.buildLockRequest(
        new LockOptions(LockMode.PESSIMISTIC_WRITE))
    .lock(post);
     
    //Hibernate doesn't know if the entity is dirty
    assertEquals("Hibernate Training",
        post.getName());
 
    //The Post entity graph is attached
    containsPost(session, post, true);
});
doInTransaction(session -> {
    //The detached Post entity changes have been lost
    Post _post = (Post) session.get(Post.class, 1L);
    assertEquals("Hibernate Master Class",
        _post.getName());
});

Запрос на блокировку повторно связывает граф сущностей, но текущий запущенный сеанс Hibernate не знает, что сущность стала грязной, находясь в отключенном состоянии. Сущность просто присоединяется без принудительного ОБНОВЛЕНИЯ или выбора текущего состояния базы данных для дальнейшего сравнения.

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

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

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

1
2
3
4
session.buildLockRequest(
    new LockOptions(LockMode.PESSIMISTIC_WRITE))
.setScope(true)
.lock(post);

Событие блокировки сущности Post распространяется на все дочерние сущности (так как мы используем CascadeType.ALL ):

1
2
3
4
select id from Comment where id = 1 for update
select id from Comment where id = 2 for update
select id from PostDetails where id = 1 for update
select id from Post where id = 1 for update

Вывод

Каскадирование замков далеко не прямолинейно и не интуитивно понятно. Явная блокировка требует усердия (чем больше блокировок мы приобретаем, тем выше вероятность мертвой блокировки), и вам все равно лучше сохранить полный контроль над распространением блокировки дочерних объектов. Аналогично лучшим практикам параллельного программирования, ручная блокировка предпочтительнее, чем автоматическое распространение блокировки.

  • Код доступен на GitHub .