Статьи

Hibernate коллекции оптимистичной блокировки

Вступление

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

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

Собственные и обратные коллекции

В реляционных базах данных две записи связаны через ссылку на внешний ключ. В этом отношении ссылочная запись является родительской, а ссылочная строка (сторона внешнего ключа) — дочерней. Ненулевой внешний ключ может ссылаться только на существующую родительскую запись.

В объектно-ориентированном пространстве эта ассоциация может быть представлена ​​в обоих направлениях. У нас может быть ссылка «многие к одному» от ребенка к родителю, а у родителя также может быть коллекция детей «один ко многим».

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

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

Однонаправленное сопоставление связей родитель-сторона-ребенок-сторона

Только родительская сторона имеет не обратную коллекцию @OneToMany для детей. Дочерняя сущность вообще не ссылается на родительскую сущность.

1
2
3
4
5
6
7
@Entity(name = "post")
public class Post {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<Comment>();
    ...
}

Однонаправленное отображение сопоставления сопоставления компонентов родительский-сторона-дочерний-дочерний

Дочерняя сторона не всегда должна быть сущностью, и мы можем вместо этого смоделировать ее как тип компонента . Встраиваемый объект (тип компонента) может содержать как базовые типы, так и сопоставления ассоциаций, но он никогда не может содержать @Id. Объект Embeddable сохраняется / удаляется вместе со своим владельцем.

Родитель имеет дочернюю ассоциацию @ElementCollection . Дочерняя сущность может ссылаться на родителя только через незапрашиваемую аннотацию @Parent, специфичную для Hibernate .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity(name = "post")
public class Post {
    ...
    @ElementCollection
    @JoinTable(name = "post_comments", joinColumns = @JoinColumn(name = "post_id"))
    @OrderColumn(name = "comment_index")
    private List<Comment> comments = new ArrayList<Comment>();
    ...
 
    public void addComment(Comment comment) {
        comment.setPost(this);
        comments.add(comment);
    }
}  
 
@Embeddable
public class Comment {
    ...
    @Parent
    private Post post;
    ...
}

Двунаправленное сопоставление связей родитель-сторона-ребенок-сторона

Родитель является владельцем, поэтому у него есть не обратная (без директивы mappedBy) @OneToMany дочерняя коллекция. Дочерняя сущность ссылается на родительскую сущность через ассоциацию @ManyToOne, которая не может быть вставлена ​​или обновлена:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Entity(name = "post")
public class Post {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<Comment>();
    ...
 
    public void addComment(Comment comment) {
        comment.setPost(this);
        comments.add(comment);
    }
}  
 
@Entity(name = "comment")
public class Comment {
    ...
    @ManyToOne
    @JoinColumn(name = "post_id", insertable = false, updatable = false)
    private Post post;
    ...
}

Двунаправленное сопоставление связей между дочерними и родительскими сторонами

Дочерняя сущность ссылается на родительскую сущность через ассоциацию @ManyToOne , а у родителя есть дочерняя коллекция mappedBy @OneToMany . Родительская сторона является обратной стороной, поэтому только изменения состояния @ManyToOne распространяются на базу данных.

Даже если есть только одна сторона-владелец, всегда рекомендуется синхронизировать обе стороны с помощью методов add / removeChild ().

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Entity(name = "post")
public class Post {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "post")
    private List<Comment> comments = new ArrayList<Comment>();
    ...
 
    public void addComment(Comment comment) {
        comment.setPost(this);
        comments.add(comment);
    }
}  
 
@Entity(name = "comment")
public class Comment {
    ...
    @ManyToOne
    private Post post; 
    ...
}

Однонаправленное сопоставление ассоциаций дочерняя сторона-родитель-родитель

Дочерняя сущность ссылается на родителя через ассоциацию @ManyToOne . Родитель не имеет дочерней коллекции @OneToMany, поэтому дочерняя сущность становится владельцем. Это сопоставление ассоциаций напоминает связь внешнего ключа реляционных данных.

1
2
3
4
5
6
7
@Entity(name = "comment")
public class Comment {
    ...
    @ManyToOne
    private Post post; 
    ...
}

Управление версиями коллекции

Раздел 3.4.2 спецификации JPA 2.1 определяет оптимистическую блокировку как:

Атрибут version обновляется средой выполнения поставщика сохраняемости при записи объекта в базу данных. Все несвязанные поля и свойства, а также все отношения, принадлежащие объекту, включены в проверки версий [35].

[35] Это включает в себя собственные отношения, поддерживаемые в таблицах соединения

Примечание. Только родительская коллекция на стороне владельца может обновлять родительскую версию.

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

Давайте проверим, как тип связи «родитель-потомок» влияет на управление версиями родителей. Поскольку нас интересует грязная проверка дочерней коллекции, однонаправленная ассоциация child-owning-side-parent будет пропущена, так как в этом случае родительский объект не содержит дочернюю коллекцию.

Прецедент

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

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
protected void simulateConcurrentTransactions(final boolean shouldIncrementParentVersion) {
    final ExecutorService executorService = Executors.newSingleThreadExecutor();
 
    doInTransaction(new TransactionCallable<Void>() {
        @Override
        public Void execute(Session session) {
            try {
                P post = postClass.newInstance();
                post.setId(1L);
                post.setName("Hibernate training");
                session.persist(post);
                return null;
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
        }
    });
 
    doInTransaction(new TransactionCallable<Void>() {
        @Override
        public Void execute(final Session session) {
            final P post = (P) session.get(postClass, 1L);
            try {
                executorService.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        return doInTransaction(new TransactionCallable<Void>() {
                            @Override
                            public Void execute(Session _session) {
                                try {
                                    P otherThreadPost = (P) _session.get(postClass, 1L);
                                    int loadTimeVersion = otherThreadPost.getVersion();
                                    assertNotSame(post, otherThreadPost);
                                    assertEquals(0L, otherThreadPost.getVersion());
                                    C comment = commentClass.newInstance();
                                    comment.setReview("Good post!");
                                    otherThreadPost.addComment(comment);
                                    _session.flush();
                                    if (shouldIncrementParentVersion) {
                                        assertEquals(otherThreadPost.getVersion(), loadTimeVersion + 1);
                                    } else {
                                        assertEquals(otherThreadPost.getVersion(), loadTimeVersion);
                                    }
                                    return null;
                                } catch (Exception e) {
                                    throw new IllegalArgumentException(e);
                                }
                            }
                        });
                    }
                }).get();
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
            post.setName("Hibernate Master Class");
            session.flush();
            return null;
        }
    });
}

Однонаправленное тестирование ассоциации родитель-сторона-ребенок-сторона-ребенок

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#create tables
Query:{[create table comment (id bigint generated by default as identity (start with 1), review varchar(255), primary key (id))][]}
Query:{[create table post (id bigint not null, name varchar(255), version integer not null, primary key (id))][]}
Query:{[create table post_comment (post_id bigint not null, comments_id bigint not null, comment_index integer not null, primary key (post_id, comment_index))][]}
Query:{[alter table post_comment add constraint UK_se9l149iyyao6va95afioxsrl  unique (comments_id)][]}
Query:{[alter table post_comment add constraint FK_se9l149iyyao6va95afioxsrl foreign key (comments_id) references comment][]}
Query:{[alter table post_comment add constraint FK_6o1igdm04v78cwqre59or1yj1 foreign key (post_id) references post][]}
 
#insert post in primary transaction
Query:{[insert into post (name, version, id) values (?, ?, ?)][Hibernate training,0,1]}
 
#select post in secondary transaction
Query:{[select entityopti0_.id as id1_1_0_, entityopti0_.name as name2_1_0_, entityopti0_.version as version3_1_0_ from post entityopti0_ where entityopti0_.id=?][1]}
 
#insert comment in secondary transaction
#optimistic locking post version update in secondary transaction
Query:{[insert into comment (id, review) values (default, ?)][Good post!]}
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate training,1,1,0]}
Query:{[insert into post_comment (post_id, comment_index, comments_id) values (?, ?, ?)][1,0,1]}
 
#optimistic locking exception in primary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate Master Class,1,1,0]}
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.EntityOptimisticLockingOnUnidirectionalCollectionTest$Post#1]

Однонаправленное тестирование ассоциации компонентов parent-owning-side-child

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
#create tables
Query:{[create table post (id bigint not null, name varchar(255), version integer not null, primary key (id))][]}
Query:{[create table post_comments (post_id bigint not null, review varchar(255), comment_index integer not null, primary key (post_id, comment_index))][]}
Query:{[alter table post_comments add constraint FK_gh9apqeduab8cs0ohcq1dgukp foreign key (post_id) references post][]}
 
#insert post in primary transaction
Query:{[insert into post (name, version, id) values (?, ?, ?)][Hibernate training,0,1]}
 
#select post in secondary transaction
Query:{[select entityopti0_.id as id1_0_0_, entityopti0_.name as name2_0_0_, entityopti0_.version as version3_0_0_ from post entityopti0_ where entityopti0_.id=?][1]}
Query:{[select comments0_.post_id as post_id1_0_0_, comments0_.review as review2_1_0_, comments0_.comment_index as comment_3_0_ from post_comments comments0_ where comments0_.post_id=?][1]}
 
#insert comment in secondary transaction
#optimistic locking post version update in secondary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate training,1,1,0]}
Query:{[insert into post_comments (post_id, comment_index, review) values (?, ?, ?)][1,0,Good post!]}
 
#optimistic locking exception in primary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate Master Class,1,1,0]}
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.EntityOptimisticLockingOnComponentCollectionTest$Post#1]

Двунаправленное тестирование ассоциации родитель-сторона-ребенок-сторона-ребенок

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
#create tables
Query:{[create table comment (id bigint generated by default as identity (start with 1), review varchar(255), post_id bigint, primary key (id))][]}
Query:{[create table post (id bigint not null, name varchar(255), version integer not null, primary key (id))][]}
Query:{[create table post_comment (post_id bigint not null, comments_id bigint not null)][]}
Query:{[alter table post_comment add constraint UK_se9l149iyyao6va95afioxsrl  unique (comments_id)][]}
Query:{[alter table comment add constraint FK_f1sl0xkd2lucs7bve3ktt3tu5 foreign key (post_id) references post][]}
Query:{[alter table post_comment add constraint FK_se9l149iyyao6va95afioxsrl foreign key (comments_id) references comment][]}
Query:{[alter table post_comment add constraint FK_6o1igdm04v78cwqre59or1yj1 foreign key (post_id) references post][]}
 
#insert post in primary transaction
Query:{[insert into post (name, version, id) values (?, ?, ?)][Hibernate training,0,1]}
 
#select post in secondary transaction
Query:{[select entityopti0_.id as id1_1_0_, entityopti0_.name as name2_1_0_, entityopti0_.version as version3_1_0_ from post entityopti0_ where entityopti0_.id=?][1]}
Query:{[select comments0_.post_id as post_id1_1_0_, comments0_.comments_id as comments2_2_0_, entityopti1_.id as id1_0_1_, entityopti1_.post_id as post_id3_0_1_, entityopti1_.review as review2_0_1_, entityopti2_.id as id1_1_2_, entityopti2_.name as name2_1_2_, entityopti2_.version as version3_1_2_ from post_comment comments0_ inner join comment entityopti1_ on comments0_.comments_id=entityopti1_.id left outer join post entityopti2_ on entityopti1_.post_id=entityopti2_.id where comments0_.post_id=?][1]}
 
#insert comment in secondary transaction
#optimistic locking post version update in secondary transaction
Query:{[insert into comment (id, review) values (default, ?)][Good post!]}
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate training,1,1,0]}
Query:{[insert into post_comment (post_id, comments_id) values (?, ?)][1,1]}
 
#optimistic locking exception in primary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate Master Class,1,1,0]}
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.EntityOptimisticLockingOnBidirectionalParentOwningCollectionTest$Post#1]

Двунаправленное тестирование связи между родителями и родителями

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
#create tables
Query:{[create table comment (id bigint generated by default as identity (start with 1), review varchar(255), post_id bigint, primary key (id))][]}
Query:{[create table post (id bigint not null, name varchar(255), version integer not null, primary key (id))][]}
Query:{[alter table comment add constraint FK_f1sl0xkd2lucs7bve3ktt3tu5 foreign key (post_id) references post][]}
 
#insert post in primary transaction
Query:{[insert into post (name, version, id) values (?, ?, ?)][Hibernate training,0,1]}
 
#select post in secondary transaction
Query:{[select entityopti0_.id as id1_1_0_, entityopti0_.name as name2_1_0_, entityopti0_.version as version3_1_0_ from post entityopti0_ where entityopti0_.id=?][1]}
 
#insert comment in secondary transaction
#post version is not incremented in secondary transaction
Query:{[insert into comment (id, post_id, review) values (default, ?, ?)][1,Good post!]}
Query:{[select count(id) from comment where post_id =?][1]}
 
#update works in primary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate Master Class,1,1,0]}

Отмена управления версиями коллекции по умолчанию

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

Давайте отменим механизм обновления родительской версии по умолчанию для двунаправленной ассоциации parent-owning-side-child :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity(name = "post")
public class Post {
    ...
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @OptimisticLock(excluded = true)
    private List<Comment> comments = new ArrayList<Comment>();
    ...
 
    public void addComment(Comment comment) {
        comment.setPost(this);
        comments.add(comment);
    }
}  
 
@Entity(name = "comment")
public class Comment {
    ...
    @ManyToOne
    @JoinColumn(name = "post_id", insertable = false, updatable = false)
    private Post post;
    ...
}

На этот раз изменения дочерней коллекции не приведут к обновлению родительской версии:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
#create tables
Query:{[create table comment (id bigint generated by default as identity (start with 1), review varchar(255), post_id bigint, primary key (id))][]}
Query:{[create table post (id bigint not null, name varchar(255), version integer not null, primary key (id))][]}
Query:{[create table post_comment (post_id bigint not null, comments_id bigint not null)][]}
Query:{[alter table post_comment add constraint UK_se9l149iyyao6va95afioxsrl  unique (comments_id)][]}
Query:{[alter table comment add constraint FK_f1sl0xkd2lucs7bve3ktt3tu5 foreign key (post_id) references post][]}
Query:{[alter table post_comment add constraint FK_se9l149iyyao6va95afioxsrl foreign key (comments_id) references comment][]}
Query:{[alter table post_comment add constraint FK_6o1igdm04v78cwqre59or1yj1 foreign key (post_id) references post][]}
 
#insert post in primary transaction
Query:{[insert into post (name, version, id) values (?, ?, ?)][Hibernate training,0,1]}
 
#select post in secondary transaction
Query:{[select entityopti0_.id as id1_1_0_, entityopti0_.name as name2_1_0_, entityopti0_.version as version3_1_0_ from post entityopti0_ where entityopti0_.id=?][1]}
Query:{[select comments0_.post_id as post_id1_1_0_, comments0_.comments_id as comments2_2_0_, entityopti1_.id as id1_0_1_, entityopti1_.post_id as post_id3_0_1_, entityopti1_.review as review2_0_1_, entityopti2_.id as id1_1_2_, entityopti2_.name as name2_1_2_, entityopti2_.version as version3_1_2_ from post_comment comments0_ inner join comment entityopti1_ on comments0_.comments_id=entityopti1_.id left outer join post entityopti2_ on entityopti1_.post_id=entityopti2_.id where comments0_.post_id=?][1]}
 
#insert comment in secondary transaction
Query:{[insert into comment (id, review) values (default, ?)][Good post!]}
Query:{[insert into post_comment (post_id, comments_id) values (?, ?)][1,1]}
 
#update works in primary transaction
Query:{[update post set name=?, version=? where id=? and version=?][Hibernate Master Class,1,1,0]}

Вывод

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

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