Статьи

Факты гибернации: знание порядка операций очистки

Hibernate меняет мышление разработчика с мышления SQL на переходы состояний мышления объекта. Согласно Hibernate Docs сущность может находиться в одном из следующих состояний:

  • new / transient: объект не связан с постоянным контекстом, будь то вновь созданный объект, о котором база данных ничего не знает.
  • постоянный: объект связан с постоянным контекстом (находится в кэше 1-го уровня), и существует строка базы данных, представляющая этот объект.
  • detached: объект ранее был связан с контекстом постоянства, но контекст постоянства был закрыт, или объект был удален вручную.
  • удалено: объект был помечен как удаленный, и контекст сохранения удалит его из базы данных во время сброса.

Перемещение объекта из одного состояния в другое выполняется путем вызова методов EntityManager, таких как:

  • сохраняются ()
  • слияния ()
  • удалять()

Каскадирование позволяет передавать данное событие от родителя к потомку, а также упрощает управление отношениями между сущностями.

Во время сброса Hibernate преобразует изменения, записанные текущим контекстом постоянства, в запросы SQL.

Теперь подумайте, что происходит в следующем коде (сокращено для краткости):

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@Entity
public class Product {
 
   @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "product", orphanRemoval = true)
   @OrderBy("index")
   private Set images = new LinkedHashSet();
 
   public Set getImages() {
      return images;
   }
 
   public void addImage(Image image) {
      images.add(image);
      image.setProduct(this);
   }
 
   public void removeImage(Image image) {
      images.remove(image);
      image.setProduct(null);
   }
}
 
@Entity
public class Image {
 
   @Column(unique = true)
   private int index;
 
   @ManyToOne
   private Product product;
 
   public int getIndex() {
      return index;
   }
 
   public void setIndex(int index) {
      this.index = index;
   }
 
   public Product getProduct() {
      return product;
   }
 
   public void setProduct(Product product) {
      this.product = product;
   }
}
 
final Long productId = transactionTemplate.execute(new TransactionCallback() {
   @Override
   public Long doInTransaction(TransactionStatus transactionStatus) {
      Product product = new Product();
 
      Image frontImage = new Image();
      frontImage.setIndex(0);
 
      Image sideImage = new Image();
      sideImage.setIndex(1);
 
      product.addImage(frontImage);
      product.addImage(sideImage);
 
      entityManager.persist(product);
      return product.getId();
   }
});
 
try {
   transactionTemplate.execute(new TransactionCallback() {
      @Override
         public Void doInTransaction(TransactionStatus transactionStatus) {
         Product product = entityManager.find(Product.class, productId);
         assertEquals(2, product.getImages().size());
         Iterator imageIterator = product.getImages().iterator();
 
         Image frontImage = imageIterator.next();
         assertEquals(0, frontImage.getIndex());
         Image sideImage = imageIterator.next();
         assertEquals(1, sideImage.getIndex());
 
         Image backImage = new Image();
         sideImage.setName("back image");
         sideImage.setIndex(1);
 
         product.removeImage(sideImage);
         product.addImage(backImage);
 
         entityManager.flush();
         return null;
     }
});
   fail("Expected ConstraintViolationException");
} catch (PersistenceException expected) {
   assertEquals(ConstraintViolationException.class, expected.getCause().getClass());
}

Из-за уникального ограничения Image.index мы получаем исключение ConstraintviolationException во время сброса.

Вы можете задаться вопросом, почему это происходит, поскольку мы вызываем remove для sideImage до добавления backImage с тем же индексом, и ответом является порядок операций сброса.

Согласно Hibernate JavaDocs порядок операций SQL:

  • вставки
  • обновления
  • удаления элементов коллекций
  • вставки элементов коллекции
  • удалений

Поскольку наша коллекция изображений «mappedBy», Image будет управлять связью, поэтому вставка «backImage» происходит до удаления «sideImage».

1
2
3
4
select product0_.id as id1_5_0_, product0_.name as name2_5_0_ from Product product0_ where product0_.id=?
select images0_.product_id as product_4_5_1_, images0_.id as id1_1_1_, images0_.id as id1_1_0_, images0_.index as index2_1_0_, images0_.name as name3_1_0_, images0_.product_id as product_4_1_0_ from Image images0_ where images0_.product_id=? order by images0_.index
insert into Image (id, index, name, product_id) values (default, ?, ?, ?)
ERROR: integrity constraint violation: unique constraint or index violation; UK_OQBG3YIU5I1E17SL0FEAWT8PE table: IMAGE

Чтобы это исправить, вы должны вручную сбросить контекст сохраняемости после операции удаления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
transactionTemplate.execute(new TransactionCallback<Void>() {
   @Override
   public Void doInTransaction(TransactionStatus transactionStatus) {
      Product product = entityManager.find(Product.class, productId);
      assertEquals(2, product.getImages().size());
      Iterator<Image> imageIterator = product.getImages().iterator();
 
      Image frontImage = imageIterator.next();
      assertEquals(0, frontImage.getIndex());
      Image sideImage = imageIterator.next();
      assertEquals(1, sideImage.getIndex());
 
      Image backImage = new Image();
      backImage.setIndex(1);
 
      product.removeImage(sideImage);
      entityManager.flush();
 
      product.addImage(backImage);
 
      entityManager.flush();
      return null;
   }
});

Это выведет желаемое поведение:

1
2
3
select versions0_.image_id as image_id3_1_1_, versions0_.id as id1_8_1_, versions0_.id as id1_8_0_, versions0_.image_id as image_id3_8_0_, versions0_.type as type2_8_0_ from Version versions0_ where versions0_.image_id=? order by versions0_.type
delete from Image where id=?
insert into Image (id, index, name, product_id) values (default, ?, ?, ?)
  • Исходный код доступен здесь .