首页 > 解决方案 > JPA 1:N 关系删除子项不会将其从父项中删除

问题描述

我有以下对象:

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity(name="Group")
public class Group {
    @Id
    @GeneratedValue
    @NotNull
    @Column(name = "GROUP_ID")
    private Long id;

    @Column(name="NAME")
    private String name;

    @OneToMany(
            targetEntity = Product.class,
            mappedBy = "groupId",
            cascade = CascadeType.ALL,
            fetch = FetchType.EAGER,
            orphanRemoval = true
    )
    private List<Product> products = new ArrayList<>();

    public Group(String name) {
        this.name = name;
    }
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity(name="Product")
public class Product {
    @Id
    @GeneratedValue
    @NotNull
    @Column(name="PRODUCT_ID")
    private Long id;

    @Column(name="NAME")
    private String name;

    @Column(name="DESCRIPTION")
    private String description;

    @Column(name="PRICE")
    private double price;

    @ManyToMany
    @JoinTable(
            name = "JOIN_PRODUCT_CART",
            joinColumns = {@JoinColumn(name = "PRODUCT_ID", referencedColumnName = "PRODUCT_ID")},
            inverseJoinColumns = {@JoinColumn(name = "CART_ID", referencedColumnName = "CART_ID")}
    )
    private List<CartEntity> carts = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "GROUP_ID")
    private Group groupId;

    public Product(String name, String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    public Product(String name, String description, double price, Group groupId) {
        this(name, description, price);
        this.groupId = groupId;
    }

    public void addToCart(CartEntity cart) {
        this.carts.add(cart);
        cart.getProductsList().add(this);
    }

    public void addGroup(Group group) {
        group.getProducts().add(this);
        this.groupId = group;
    }
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "cart")
public class CartEntity {

    @Id
    @NotNull
    @GeneratedValue
    @Column(name = "CART_ID")
    private Long id;

    @ManyToMany(cascade = CascadeType.ALL, mappedBy = "carts")
    private List<Product> productsList = new ArrayList<>();

    public void addProduct(Product product) {
        productsList.add(product);
        product.getCarts().add(this);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CartEntity that = (CartEntity) o;
        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

现在,当我进行以下测试时:

public class ProductDaoTestSuite {
    @Autowired
    private ProductDao productDao;
    @Autowired
    private CartDaoStub cartDaoStub;
    @Autowired
    private GroupDao groupDao;

    @Test
    public void testDeleteProduct() {
        // Given
        Product product = new Product("test", "testProduct", 100.0);
        Group group = new Group("group1");
        CartEntity cart = new CartEntity();

        product.addGroup(group);
        cart.addProduct(product);

        // When
        groupDao.save(group);
        productDao.save(product);
        cartDaoStub.save(cart);

        Long groupId = group.getId();
        Long productId = product.getId();
        Long cartId = cart.getId();

        productDao.deleteById(productId);

        // Then
        Assert.assertTrue(cartDaoStub.findById(cartId).isPresent());
        Assert.assertEquals(0, cartDaoStub.findById(cartId).get().getProductsList().size());

        Assert.assertTrue(groupDao.findById(groupId).isPresent());
        Assert.assertEquals(0, groupDao.findById(groupId).get().getProducts().size());

删除产品后,我希望与它在组和购物车中的关联消失(产品从他们的列表关系字段中消失)。但是,目前还没有发生这种情况。当我在删除产品后使用 Group/Cart Dao 从数据库中提取组和购物车时,他们的列表中仍然有产品,而从数据库中提取的产品返回为 null。我试图为@OneToMany 注释添加“orphanRemoval = true”值,但它似乎不适用于 Group 实体。

我究竟做错了什么?

我已经开始尝试在 Product 类的 @ManyToOne 中添加所有类型的级联(除了 REMOVE),但到目前为止还没有运气。

标签: javahibernatejpa

解决方案


对于 1:N,您的微调应该可以正常工作。

失败的原因:在执行“groupDao.save(group);”时 现在处于持久性上下文中,调用“groupDao.findById(groupId).get().getProducts().size()”将返回来自持久性上下文的副本。

要解决这个问题:只需添加: entityManager.flush(); 和 entityManager.clear(); 断言之前

我想用这个集成测试来证明它

    @Test
    @Transactional
    public void deleteProduct_groupShouldNowBeEmpty() {
        ProductGroup group = groupRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
        Assert.assertEquals(1, group.getProducts().size());

        Product product = productRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
        productRepository.delete(product);

        entityManager.flush();
        entityManager.clear();

        Assert.assertEquals(0, productRepository.findAll().size());
        Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());
    }

如果我们要删除前 2 行,那么我们不需要冲洗和清除。像这样。

    @Test
    @Transactional
    public void deleteProduct_groupShouldNowBeEmpty() {
        Product product = productRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
        productRepository.delete(product);

        Assert.assertEquals(0, productRepository.findAll().size());
        Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());
    }

对于 N:M,由于将有另一个表正在引用产品,因此我们需要先从该表中删除记录,然后再删除产品。

N:M 有点棘手,所以如果我可以建议更改域,我将在此处进行操作。(集成测试在底部。)

我将添加一个单独的实体:与ProductCart 关联的CartItem

@Entity
public class CartItem {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    @ManyToOne
    private Product product;

    @ManyToOne
    private Cart cart;

    public String getId() {
        return id;
    }

    // Required by JPA
    protected CartItem() {}

}

对于产品实体:添加与 CartItem 的双向关系

@Entity
public class Product {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    private String name;

    private String description;

    private BigDecimal price;

    @ManyToOne
    private ProductGroup group;

    @OneToMany(mappedBy = "product")
    private List<CartItem> cartItems;

    public List<CartItem> getCartItems() {
        return cartItems;
    }

    // Required by JPA
    protected Product() {}
}

然后,检索产品(使用 Join Fetch 避免 N+1,因为稍后将循环遍历每个 cartItem)

public interface ProductRepository extends JpaRepository<Product, String> {

    @Query("SELECT product FROM Product product JOIN FETCH product.cartItems")
    Optional<Product> findProduct(String Id);

}

在 CartItemRepository 中创建另一个查询以按 id 批量删除 cartItems

public interface CartItemRepository extends JpaRepository<CartItem, String> {

    @Modifying
    @Query("DELETE FROM CartItem cartItem WHERE cartItem.id IN :ids")
    void deleteByIds(@Param("ids") List<String> ids);

}

最后是整合测试来总结一切:

@Test
@Transactional
public void deleteProduct_associatedWithCart() {
    Cart cart = cartRepository.findById("0001").get();
    Assert.assertEquals(1, cart.getCartItems().size());

    Product product = productRepository.findProduct("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
    List<String> cartItemIds = product.getCartItems().stream()
            .map(CartItem::getId)
            .collect(Collectors.toList());

    cartItemRepository.deleteByIds(cartItemIds);
    productRepository.delete(product);

    entityManager.flush();
    entityManager.clear();

    Assert.assertEquals(0, productRepository.findAll().size());
    Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());

    Assert.assertEquals(0, cartItemRepository.findAll().size());
    Assert.assertEquals(0, cartRepository.findById("0001").get().getCartItems().size());
}

我已经在这个集成测试中使用了 DBUnit,所以我认为共享数据集也会很有帮助。

    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
        <product_group id="0001" name="product group with 1 product"/>
        <product id="0001" group_id="0001" />
    
        <cart id="0001" />
        <cart_item id="0001" product_id="0001" cart_id="0001" />
    </dataset>

推荐阅读