Вступление
Введя поддержку явной блокировки Hibernate, а также Cascade Types , пришло время проанализировать поведение CascadeType.LOCK .
Запрос блокировки Hibernate запускает внутренний LockEvent . Связанный DefaultLockEventListener может каскадно запросить блокировку для дочерних объектов блокировки.
Поскольку CascadeType.ALL также включает CascadeType.LOCK , стоит понимать, когда запрос блокировки распространяется от родительского объекта к дочернему объекту.
Время тестирования
Начнем со следующей модели сущности:
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 .
Ссылка: | Hibernate CascadeType.LOCK получил от нашего партнера JCG Влада Михалча в блоге Влада Михалча . |