I have a wired problem where Hibernate triggers some delete queries while updating an entity.
The entity:
public class Item {
@Id
@GeneratedValue
private Long id;
@ManyToMany(cascade = CascadeType.PERSIST)
@JoinTable(name = "item_tags", joinColumns = @JoinColumn(name = "item_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();
// some more stuff
}
The code to update the entity:
public Item updateItem(ItemUpdateRequest dto, long id) {
Item item = itemDao.findEntity(id);
item = ItemMapper.INSTANCE.mapFromUpdateRequest(item, dto, tagDao);
// save item so it can be returned
item = itemDao.saveEntity(item);
return item;
}
The ItemMapper is generated by MapStruct with custom methods to load the tags since the request only sends the IDs of the tags:
@AfterMapping
default void loadTags(@MappingTarget Item item, ItemUpdateRequest dto, @Context TagDAO dao) {
Collection<Tag> tags = dao.findEntities(dto.getTags());
item.setTags(new HashSet<>(tags));
}
The generated MapStruct mapper therefore looks as follows:
@Override
public Item mapFromUpdateRequest(Item item, ItemUpdateRequest dto, TagDAO tagDao) {
if ( dto == null ) {
return null;
}
item.setDescription( dto.getDescription() );
item.setPrice( dto.getPrice() );
item.setTitle( dto.getTitle() );
item.setType( itemTypeMapper.map( dto.getType() ) );
loadTags( item, dto, tagDao );
return item;
}
If the ItemUpdateRequest contains no tags the following queries are executed by Hibernate:
1. Hibernate: update public.item set description=?, price=?,
restaurant_id=?, title=?, type=?, visible=? where id=?
2. Hibernate: select tags0_.item_id as item_id1_20_0_, tags0_.tag_id as
tag_id2_20_0_, tag1_.id as id1_41_1_, tag1_.abbreviation as
abbrevia2_41_1_, tag1_.text as text3_41_1_ from public.item_tags
tags0_ inner join public.tag tag1_ on tags0_.tag_id=tag1_.id where
tags0_.item_id=?
3. Hibernate: delete from public.item_tags where item_id=?
The I don't see why Hibernate would issue the delete queries. Neither the item from the DB nor the ItemUpdateRequest contain any tags. I though that maybe Hibernate recognizes the changed collection, since it is set again in the loadTags() method.
I tried writing a unit test, but this test works as expected and no delete statements are issued.
@Test
public void deleteQueryTest() {
Item item = Item.builder().title("Test").build();
item = itemDao.saveEntity(item);
Item reloadedItem = itemDao.findEntity(item.getId());
ItemUpdateRequest updateRequest = ItemUpdateRequest.builder()
.title("Test Changed")
.build();
reloadedItem = ItemMapper.INSTANCE.mapFromUpdateRequest(reloadedItem, updateRequest, tagDao);
reloadedItem = itemDao.saveEntity(reloadedItem);
}
CodePudding user response:
Your loadTags
method always sets a new collection instance instead of changing the existing collection instance that supports dirty tracking. Try to use item.getTags().clear(); item.getTags().addAll(tags);
instead. Apart from that, I would recommend you look into Blaze-Persistence Entity Views which can further improve the transaction performance by doing fewer queries.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
@EntityView(Item.class)
@UpdatableEntityView
public interface ItemDto {
@IdMapping
Long getId();
String getTitle();
void setTitle(String title);
String getDescription();
void setDescription(String description);
BigDecimal getPrice();
void setPrice(BigDecimal price);
ItemTypeDto getType();
void setType(ItemTypeDto type);
Set<TagDto> getTags();
@EntityView(ItemType.class)
interface ItemTypeDto {
@IdMapping
Long getId();
}
@EntityView(Tag.class)
@UpdatableEntityView
@CreatableEntityView
interface TagDto {
@IdMapping
Long getId();
String getAbbreviation();
void setAbbreviation(String abbreviation);
String getText();
void setText(String text);
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ItemDto a = entityViewManager.find(entityManager, ItemDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<ItemDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
Thanks to the Jackson and Spring MVC integration your code could look as simple as this:
public interface ItemDtoRepository extends EntityViewRepository<ItemDto, Long> {
}
@Autowired
private ItemDtoRepository itemRepository;
@RequestMapping(path = "/items/{id}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> updateItem(@EntityViewId("id") @RequestBody ItemDto itemDto) {
itemRepository.save(itemDto);
return ResponseEntity.ok(itemDto.getId().toString());
}
For more details you can also look into the documentation: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#entity-view-deserialization