Статьи

Удаление одним выстрелом в спящем режиме (JPA)

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

Раздел удаления из одного кадра говорит:

Удаление элементов коллекции один за другим иногда может быть крайне неэффективным. Hibernate знает, что не следует делать в случае новой пустой коллекции (например, если вы вызвали list.clear() ). В этом случае Hibernate выдаст один DELETE .

Предположим, вы добавили один элемент в коллекцию размером двадцать, а затем удалили два элемента. Hibernate выдаст одну INSERT и две инструкции DELETE , если коллекция не является сумкой. Это, конечно, желательно.

Однако предположим, что мы удалили восемнадцать элементов, оставив два, а затем добавили новые элементы. Есть два возможных способа продолжить

  • удалить восемнадцать строк одну за другой, а затем вставить три строки
  • удалить всю коллекцию в одном SQL DELETE и вставить все пять текущих элементов по одному

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

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

Удаление одним выстрелом не применяется к коллекциям, сопоставленным inverse="true" .

Значение inverse="true" предназначено для (Hibernate Mapping) XML. Но в этом посте мы увидим, как «одноразовое удаление» работает в JPA (с Hibernate в качестве поставщика).

Мы попробуем разные подходы и посмотрим, какой из них приведет к удалению одним выстрелом.

  1. Двунаправленный один-ко-многим
  2. Однонаправленный один-ко-многим (с соединительным столом)
  3. Однонаправленный один-ко-многим (без таблицы соединений)
  4. ElementCollection один-ко-многим (используя ElementCollection )

Мы будем использовать сущность Cart со многими CartItem s.

Двунаправленный один-ко-многим

Для этого у нас есть ссылки с обеих сторон.

01
02
03
04
05
06
07
08
09
10
@Entity
public class Cart { ...
 @OneToMany(mappedBy="cart", cascade=ALL, orphanRemoval=true)
 Collection<OrderItem> items;
}
 
@Entity
public class CartItem { ...
 @ManyToOne Cart cart;
}

Чтобы проверить это, мы вставляем одну строку в таблицу для Cart и три или более строк в таблицу для CartItem . Затем мы запускаем тест.

01
02
03
04
05
06
07
08
09
10
11
public class CartTests { ...
 @Test
 public void testOneShotDelete() throws Exception {
  Cart cart = entityManager.find(Cart.class, 53L);
  for (CartItem item : cart.items) {
   item.cart = null; // remove reference to cart
  }
  cart.items.clear(); // as indicated in Hibernate manual
  entityManager.flush(); // just so SQL commands can be seen
 }
}

В показанных командах SQL каждый элемент был удален индивидуально (а не как одноразовое удаление).

1
2
3
delete from CartItem where id=?
delete from CartItem where id=?
delete from CartItem where id=?

Отбрасывание оригинальной коллекции тоже не сработало. Это даже вызвало исключение.

1
2
3
4
5
6
7
8
9
public class CartTests { ...
 @Test
 public void testOneShotDelete() throws Exception {
  Cart cart = entityManager.find(Cart.class, 53L);
  // remove reference to cart
  cart.items = new LinkedList<CartItem>(); // discard, and use new collection
  entityManager.flush(); // just so SQL commands can be seen
 }
}
1
2
3
javax.persistence.PersistenceException:
    org.hibernate.HibernateException:
        A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: ….Cart.items

Я проверял это с Hibernate 4.3.11 и HSQL 2.3.2. Если ваши результаты отличаются, пожалуйста, нажмите на комментарии .

Однонаправленный «один ко многим» (с таблицей соединений)

Для этого мы вносим изменения в отображение. Это приводит к созданию таблицы соединения.

01
02
03
04
05
06
07
08
09
10
@Entity
public class Cart { ...
 @OneToMany(cascade=ALL)
 Collection<OrderItem> items;
}
 
@Entity
public class CartItem { ...
 // no @ManyToOne Cart cart;
}

Опять же, мы вставляем одну строку в таблицу для Cart и три или более строк в таблицу для CartItem . Мы также должны вставить соответствующие записи в таблицу соединений ( Cart_CartItem ). Затем мы запускаем тест.

1
2
3
4
5
6
7
8
public class CartTests { ...
 @Test
 public void testOneShotDelete() throws Exception {
  Cart cart = entityManager.find(Cart.class, 53L);
  cart.items.clear(); // as indicated in Hibernate manual
  entityManager.flush(); // just so SQL commands can be seen
 }
}

Показанные команды SQL удалили связанные строки в таблице соединений (одной командой). Но строки в таблице для CartItem все еще существуют (и не удаляются).

1
2
delete from Cart_CartItem where cart_id=?
// no delete commands for CartItem

Хм, не совсем то, что мы хотим, так как строки в таблице для CartItem все еще существуют.

Однонаправленный «один ко многим» (без таблицы соединений)

Начиная с JPA 2.0, таблицу соединений можно избежать в однонаправленном режиме «один ко многим», указав @JoinColumn .

01
02
03
04
05
06
07
08
09
10
11
@Entity
public class Cart { ...
 @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
 @JoinColumn(name="cart_id", updatable=false, nullable=false)
 Collection<OrderItem> items;
}
 
@Entity
public class CartItem { ...
 // no @ManyToOne Cart cart;
}

Опять же, мы вставляем одну строку в таблицу для Cart и три или более строк в таблицу для CartItem . Затем мы запускаем тест.

1
2
3
4
5
6
7
8
public class CartTests { ...
 @Test
 public void testOneShotDelete() throws Exception {
  Cart cart = entityManager.find(Cart.class, 53L);
  cart.items.clear(); // as indicated in Hibernate manual
  entityManager.flush(); // just so SQL commands can be seen
 }
}

Отказ от оригинальной коллекции тоже не сработал. Это также вызвало то же исключение (как и в случае двунаправленного «один ко многим»).

1
2
3
javax.persistence.PersistenceException:
    org.hibernate.HibernateException:
        A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: ….Cart.items

ElementCollection One-to-Many (с ElementCollection )

JPA 2.0 представила @ElementCollection . Это позволяет устанавливать отношения один-ко-многим, причем многие стороны являются либо @Basic либо @Embeddable (т.е. не @Entity ).

01
02
03
04
05
06
07
08
09
10
11
12
13
@Entity
public class Cart { ...
 @ElementCollection // @OneToMany for basic and embeddables
 @CollectionTable(name="CartItem") // defaults to "Cart_items" if not overridden
 Collection<OrderItem> items;
}
 
@Embeddable // not an entity!
public class CartItem {
 // no @Id
 // no @ManyToOne Cart cart;
 private String data; // just so that there are columns we can set
}

Опять же, мы вставляем одну строку в таблицу для Cart и три или более строк в таблицу для CartItem . Затем мы запускаем тест.

1
2
3
4
5
6
7
8
public class CartTests { ...
 @Test
 public void testOneShotDelete() throws Exception {
  Cart cart = entityManager.find(Cart.class, 53L);
  cart.items.clear(); // as indicated in Hibernate manual
  entityManager.flush(); // just so SQL commands can be seen
 }
}

Yey! Связанные строки для CartItem были удалены за один раз.

1
delete from CartItem where Cart_id=?

Заключительные мысли

Удаление одним выстрелом происходит с однонаправленным «один ко многим» с использованием ElementCollection (где сторона с множеством сторон является вложенной, а не сущностью).

В сценарии однонаправленной связи «один ко многим» с таблицей соединений удаление записей в таблице соединений не приносит особой пользы.

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

Теперь это не означает, что вы должны использовать ElementCollection все время. Возможно, удаление одним выстрелом относится только к совокупным корням. В этих случаях использование Embeddable и ElementCollection может быть подходящим для коллекции объектов-значений, которые составляют агрегат. Когда удален совокупный корень, было бы хорошо видеть, что «дочерние» объекты также должны быть удалены (и эффективным способом).

Мне бы хотелось, чтобы в JPA был способ указать, что дочерние сущности находятся в частной собственности и могут быть безопасно удалены при удалении родительской сущности (например, аналогично @PrivateOwned в EclipseLink). Посмотрим, будет ли он включен в будущую версию API.

Надеюсь это поможет.