Вступление
В моем предыдущем посте я объяснил конфигурации 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
|
@Testpublic 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
|
@Testpublic 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 Влада Михалча в блоге Влада Михалча . |
