Как работает flush в Hibernate?

Введение

В этой статье мы поговорим об операции flush. Операция flush позволяет нам сихронизировать in-memory состояние Persistence контекста с БД (т.е. записывает изменения в БД). Но сначала обсудим состояния сущности.

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

  • new/transient:  состояние, в котором, новый созданный объект, о чьем существовании не знает СУБД и он не привязан к persistance контексту.
  • persistent: сущность привязана к persistence контексту (находясь в кэше первого уровня) и есть запись в БД отбражающая эту сущность.
  • detached: сущность была привязана к persistence контексту, но контекст закрылся, а сущность осталась.
  • removed: сущность в данном состоянии помечена как удаленная и persistence контекст удалит ее из БД, когда наступит flush-time.

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

  • persist()
  • merge()
  • remove()

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

В течение flush time, Hibernate транслирует изменения записанные текущим Persistence контекстом в SQL запросы.

Когда происходит flush?

Flush происходит в трех ситуациях:

  • Когды вы делает commit транзакции Hibernate
  • До того как выполняется запрос в БД
  • Когда вы вызываете entityManager.flush()

Особенно важно понять вторую из этих ситуаций — entityManager делает flush до выполнения запроса. Это происходит не для каждого запроса! Важно учесть, что цель сессии Hibernate минимизировать количество операций записи в БД, поэтому она не делает flush, когда не считает это необходимым.

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

Например, предположим, что у меня есть объекты Customer и Order, Customer имеет коллекцию Order.

Order order1 = new Order();
order1.setOrderTotal(29.99);
 
Order order2 = new Order();
order2.setOrderTotal(8.99);
 
Order order3 = new Order();
order3.setOrderTotal(15.99);
 
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
 
session.save(order1);
session.save(order2);
session.save(order3);

На этом этапе в БД сохранятся три записи. Теперь создадим customer и сохраним его:

Customer customer = new Customer();
customer.setForename("Hedley");
customer.setSurname("Proctor");
session.save(customer);

Хорошо, теперь customer в БД. Теперь создадим связь между ними:

order1.setCustomer(customer);
order2.setCustomer(customer);
order3.setCustomer(customer);

Теперь сессия Hibernate «грязная» в том смысле что, изменения в сессии не были еще сохранены в БД.
Если мы выполним SQL запрос, то увидим что результаты не отражают реального состояния.

Query query = session.createSQLQuery("select id,customer_id from orders");
List results = query.list();

Вот результаты:


Hibernate: select id,customer_id from orders
1,null
2,null
3,null

Даже выполнив запрос в HQL или используя Criteria API мы не вызовем flush:

Query query = session.createQuery("from Customer");

Hibernate выполнит flush только при выполнении запроса, в котором участвуют «грязные» объекты, например:

Query query = session.createQuery("from Order");

Порядок выполнения

Давайте теперь разберемся, что происходит в этом примере:


@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();
 backImage.setName("back image");
 backImage.setIndex(1);
 
 product.removeImage(sideImage);
 product.addImage(backImage);
 
 entityManager.flush();
 return null;
 }
});
 fail("Expected ConstraintViolationException");
} catch (PersistenceException expected) {
 assertEquals(ConstraintViolationException.class, expected.getCause().getClass());
}

Мы получаем ConstraintViolationException во время выполнения flush, потому что проставили полю index класса Image свойство уникальности @Column(unique = true). 

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

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

  • INSERT
  • UPDATE
  • DELETE элементов коллекций
  • INSERT элементов коллекций
  • DELETE

Из-за того что наша Image коллекция помечена @mappedBy, класс Image контролирует связь, следовательно добавление backImage произойдет раньше, чем удаление sideImage.


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

Для исправления необходимо выполнить отдельную операцию flush после удаления:


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;
}
});

Лог покажет нам желаемое поведение:


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, ?, ?, ?)

 

(Visited 3 124 times, 6 visits today)

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.