Вступление
В моем предыдущем посте я объяснил конфигурации Hibernate, необходимые для пакетной обработки операторов INSERT и UPDATE. Этот пост продолжит эту тему с пакетным оператором DELETE.
Объекты модели домена
Начнем со следующей модели сущности:
Сущность Post имеет связь « один ко многим» с комментарием и отношение « один к одному» с сущностью PostDetails :
1
2
3
4
5
6
7
|
@OneToMany (cascade = CascadeType.ALL, mappedBy = "post" , orphanRemoval = true ) private List<Comment> comments = new ArrayList<>(); @OneToOne (cascade = CascadeType.ALL, mappedBy = "post" , orphanRemoval = true , fetch = FetchType.LAZY) private PostDetails details; |
Предстоящие тесты будут выполняться на следующих данных:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
doInTransaction(session -> { int batchSize = batchSize(); for ( int i = 0 ; i < itemsCount(); i++) { int j = 0 ; Post post = new Post(String.format( "Post no. %d" , i)); post.addComment( new Comment( String.format( "Post comment %d:%d" , i, j++))); post.addComment( new Comment(String.format( "Post comment %d:%d" , i, j++))); post.addDetails( new PostDetails()); session.persist(post); if (i % batchSize == 0 && i > 0 ) { session.flush(); session.clear(); } } }); |
Конфигурация гибернации
Как уже объяснялось , для пакетных операторов INSERT и UPDATE необходимы следующие свойства:
1
2
3
4
5
6
7
8
|
properties.put( "hibernate.jdbc.batch_size" , String.valueOf(batchSize())); properties.put( "hibernate.order_inserts" , "true" ); properties.put( "hibernate.order_updates" , "true" ); properties.put( "hibernate.jdbc.batch_versioned_data" , "true" ); |
Далее мы собираемся проверить, упакованы ли операторы DELETE.
JPA Каскад Удалить
Поскольку каскадные переходы состояний сущностей удобны, я собираюсь доказать, что пакетная обработка CascadeType.DELETE и JDBC плохо сочетается .
Следующие тесты собираются:
- Выберите несколько сообщений вместе с комментариями и сообщениями
- Удалите сообщения , одновременно распространяя событие удаления на Комментарии и PostDetails.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Test public void testCascadeDelete() { LOGGER.info( "Test batch delete with cascade" ); final AtomicReference<Long> startNanos = new AtomicReference<>(); addDeleteBatchingRows(); doInTransaction(session -> { List<Post> posts = session.createQuery( "select distinct p " + "from Post p " + "join fetch p.details d " + "join fetch p.comments c" ) .list(); startNanos.set(System.nanoTime()); for (Post post : posts) { session.delete(post); } }); LOGGER.info( "{}.testCascadeDelete took {} millis" , getClass().getSimpleName(), TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - startNanos.get() )); } |
Запуск этого теста дает следующий вывод:
1
2
3
4
5
6
7
8
9
|
Query:{[delete from Comment where id =? and version=?][55,0]} {[delete from Comment where id =? and version=?][56,0]} Query:{[delete from PostDetails where id =?][3]} Query:{[delete from Post where id =? and version=?][3,0]} Query:{[delete from Comment where id =? and version=?][54,0]} {[delete from Comment where id =? and version=?][53,0]} Query:{[delete from PostDetails where id =?][2]} Query:{[delete from Post where id =? and version=?][2,0]} Query:{[delete from Comment where id =? and version=?][52,0]} {[delete from Comment where id =? and version=?][51,0]} Query:{[delete from PostDetails where id =?][1]} Query:{[delete from Post where id =? and version=?][1,0]} |
Были объединены только операторы Comment DELETE, остальные объекты были удалены в отдельных обходах базы данных.
Причину такого поведения дает реализация сортировки ActionQueue :
1
2
3
4
5
6
7
|
if ( session.getFactory().getSettings().isOrderUpdatesEnabled() ) { // sort the updates by pk updates.sort(); } if ( session.getFactory().getSettings().isOrderInsertsEnabled() ) { insertions.sort(); } |
В то время как INSERTS и UPDATES включены, операторы DELETE не сортируются вообще. Пакет JDBC можно использовать повторно только тогда, когда все операторы принадлежат одной и той же таблице базы данных. Когда входящий оператор нацелен на другую таблицу базы данных, текущий пакет должен быть освобожден, чтобы новый пакет соответствовал текущей таблице базы данных операторов:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
public Batch getBatch(BatchKey key) { if ( currentBatch != null ) { if ( currentBatch.getKey().equals( key ) ) { return currentBatch; } else { currentBatch.execute(); currentBatch.release(); } } currentBatch = batchBuilder().buildBatch(key, this ); return currentBatch; } |
Удаление сирот и ручная промывка
Обходной путь — отсоединить все дочерние сущности при ручной очистке сеанса Hibernate перед переходом к новой дочерней ассоциации:
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
|
@Test public void testOrphanRemoval() { LOGGER.info( "Test batch delete with orphan removal" ); final AtomicReference<Long> startNanos = new AtomicReference<>(); addDeleteBatchingRows(); doInTransaction(session -> { List<Post> posts = session.createQuery( "select distinct p " + "from Post p " + "join fetch p.details d " + "join fetch p.comments c" ) .list(); startNanos.set(System.nanoTime()); posts.forEach(Post::removeDetails); session.flush(); posts.forEach(post -> { for (Iterator<Comment> commentIterator = post.getComments().iterator(); commentIterator.hasNext(); ) { Comment comment = commentIterator.next(); comment.post = null ; commentIterator.remove(); } }); session.flush(); posts.forEach(session::delete); }); LOGGER.info( "{}.testOrphanRemoval took {} millis" , getClass().getSimpleName(), TimeUnit.NANOSECONDS.toMillis( System.nanoTime() - startNanos.get() )); } |
На этот раз все операторы DELETE правильно упакованы:
1
2
3
|
Query:{[delete from PostDetails where id =?][2]} {[delete from PostDetails where id =?][3]} {[delete from PostDetails where id =?][1]} Query:{[delete from Comment where id =? and version=?][53,0]} {[delete from Comment where id =? and version=?][54,0]} {[delete from Comment where id =? and version=?][56,0]} {[delete from Comment where id =? and version=?][55,0]} {[delete from Comment where id =? and version=?][52,0]} {[delete from Comment where id =? and version=?][51, Query:{[delete from Post where id =? and version=?][2,0]} {[delete from Post where id =? and version=?][3,0]} {[delete from Post where id =? and version=?][1,0]} |
SQL Каскад Удалить
Лучшим решением является использование каскадного удаления SQL вместо механизма распространения состояния сущности JPA . Таким образом, мы также можем уменьшить количество операторов DML . Поскольку Hibernate Session действует как кэш транзакционной записи с обратной записью , мы должны быть особенно осторожны при смешивании переходов состояния сущности с автоматическими действиями на стороне базы данных, поскольку контекст постоянства может не отражать последние изменения базы данных.
Ассоциация Комментария « один-ко-многим» сущности Post помечена специфичной для Hibernate аннотацией @OnDelete , поэтому автоматически генерируемая схема базы данных включает в себя директиву ON DELETE CASCADE :
1
2
3
4
5
6
|
@OneToMany (cascade = { CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "post" ) @OnDelete (action = OnDeleteAction.CASCADE) private List<Comment> comments = new ArrayList<>(); |
Генерация следующего DDL :
1
2
3
|
alter table Comment add constraint FK_apirq8ka64iidc18f3k6x5tc5 foreign key (post_id) references Post on delete cascade |
То же самое делается с непосредственным сопоставлением сущностей PostDetails :
1
2
3
4
5
|
@OneToOne (fetch = FetchType.LAZY) @JoinColumn (name = "id" ) @MapsId @OnDelete (action = OnDeleteAction.CASCADE) private Post post; |
И связанный DDL :
1
2
3
|
alter table PostDetails add constraint FK_h14un5v94coafqonc6medfpv8 foreign key (id) references Post on delete cascade |
CascadeType.ALL и orphanRemoval были заменены на CascadeType.PERSIST и CascadeType.MERGE , потому что мы больше не хотим, чтобы Hibernate распространял событие удаления сущности.
Тест удаляет только объекты Post.
1
2
3
4
5
6
7
8
9
|
doInTransaction(session -> { List<Post> posts = session.createQuery( "select p from Post p" ) .list(); startNanos.set(System.nanoTime()); for (Post post : posts) { session.delete(post); } }); |
Операторы DELETE правильно упакованы, поскольку существует только одна таблица назначения.
1
|
Query:{[delete from Post where id =? and version=?][1,0]} {[delete from Post where id =? and version=?][2,0]} {[delete from Post where id =? and version=?][3,0]} |
Вывод
Если пакетирование операторов INSERT и UPDATE является лишь вопросом конфигурации, операторы DELETE требуют некоторых дополнительных шагов, которые могут увеличить сложность уровня доступа к данным.
- Код доступен на GitHub .
Ссылка: | Как сделать пакет DELETE с помощью Hibernate от нашего партнера по JCG Влада Михалча в блоге Влада Михалча . |