Введение
В этой статье мы поговорим об операции 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, ?, ?, ?)