首页 > 解决方案 > 如果集合被重新初始化,Hibernate 会触发删除语句

问题描述

我有一个有线问题,即 Hibernate 在更新实体时触发一些删除查询。

实体:

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
}

更新实体的代码:

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

ItemMapper 由 MapStruct 生成,带有自定义方法来加载标签,因为请求只发送标签的 ID:

@AfterMapping
default void loadTags(@MappingTarget Item item, ItemUpdateRequest dto, @Context TagDAO dao) {
    Collection<Tag> tags = dao.findEntities(dto.getTags());
    item.setTags(new HashSet<>(tags));
}

因此,生成的 MapStruct 映射器如下所示:

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

如果 ItemUpdateRequest 不包含任何标签,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=?

我不明白为什么 Hibernate 会发出删除查询。数据库中的项目和 ItemUpdateRequest 都不包含任何标签。我认为 Hibernate 可能会识别更改的集合,因为它是在 loadTags() 方法中再次设置的。

我尝试编写单元测试,但该测试按预期工作,并且没有发出删除语句。

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

标签: hibernatespring-data-jpa

解决方案


您的loadTags方法总是设置一个新的集合实例,而不是更改支持脏跟踪的现有集合实例。尝试item.getTags().clear(); item.getTags().addAll(tags);改用。除此之外,我建议您研究Blaze-Persistence Entity Views,它可以通过减少查询来进一步提高事务性能。

我创建了该库以允许在 JPA 模型和自定义接口或抽象类定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您按照自己喜欢的方式定义目标结构(域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

使用 Blaze-Persistence Entity-Views 的用例的 DTO 模型可能如下所示:

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

查询是将实体视图应用于查询的问题,最简单的就是通过 id 进行查询。

ItemDto a = entityViewManager.find(entityManager, ItemDto.class, id);

Spring Data 集成允许您几乎像 Spring Data Projections 一样使用它:https ://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<ItemDto> findAll(Pageable pageable);

最好的部分是,它只会获取实际需要的状态!

感谢 Jackson 和 Spring MVC 的集成,您的代码看起来就像这样简单:

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

有关更多详细信息,您还可以查看文档:https ://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#entity-view-deserialization


推荐阅读