JPA 1:N关系删除子级不会将其从父级中删除

时间:2020-08-22 14:16:28

标签: java hibernate jpa

我有以下对象:

@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删除产品后从数据库中提取组和购物车时,他们的清单中仍然有产品,而从DB中提取的产品则返回null。 我尝试为@OneToMany注释添加“ orphanRemoval = true”值,但它似乎不适用于Group实体。

我在做什么错了?

我已经开始尝试将所有类型的层叠(除REMOVE除外)添加到Product类的@ManyToOne上,但是到目前为止还算不上成功。

4 个答案:

答案 0 :(得分:2)

对于1:N ,您只需稍作调整就可以正常工作。

失败的原因:执行“ groupDao.save(group);” 后,此 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());
    }

如果要删除前两行,则无需冲洗并清除。这样。

    @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有点棘手,所以如果我可以建议域更改,请按以下步骤进行操作。 (集成测试在底部。)

我将添加一个单独的实体: 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>

答案 1 :(得分:0)

remove实体时,此状态转换应从父级传播到子级,而不是相反。

在这种情况下,您需要在功能上将其移至Group实体,如下所示:

@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.LAZY, // Always prefer LAZY initialized Collections to EAGER ones
            orphanRemoval = true
    )
    private List<Product> products = new ArrayList<>();

    public Group(String name) {
        this.name = name;
    }

    public void addProduct(Product product){
      product.setGroupId(this);
      this.products.add(product);
    }

    public void removeProduct(Product product){
      product.setGroupId(null);
      this.products.remove(product);
    }

如果要removeProduct,只需调用removeProduct方法和save父实体:

Group group = new Group("group1");
Product product = new Product("test", "testProduct", 100.0);

group.addProduct(product);

groupDao.save(group);

另一方面,我们在ProductCartEntity之间有多对多关系。

首先,如果您将CartEntity实体配置为Cascade.ALL,如示例所示:

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

这可能会产生不良影响:如果删除CartEntity,即使其他Product仍然存在,它也会删除与该实体关联的所有CartEntity与他们相关联。 Vlad Mihalcea在this article中对其进行了详细说明。

为避免该问题,最好的选择是将关系定义如下:

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

这将为我们提供一个CartEntity,如下所示:

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "cart")
public class CartEntity {

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

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

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

    public void removeProduct(Product product) {
        productsList.remove(product);
        product.getCarts().remove(this);
    }

    public void removeProducts() {
        for(Product product : new ArrayList<>(products)) {
            removeProduct(product);
        }
    }

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

请注意,包含了removeProductremoveProducts方法。

使用此代码,如果您需要删除CartEntity,只需执行以下操作:

cart.removeProducts();
cartDao.remove(cart);

如果您需要从remove ProductCartEntity(只会删除该关系)

cart.removeProduct(product);
cartDao.save(cart);

如果您需要将Product remove传播到CartEntity,我认为最好的选择是创建一个照顾整个过程的业务方法。像这样思考:

public void removeProduct(Product product){
  Group group = product.getGroupId();
  group.removeProduct(product);

  final List<CartEntity> carts = product.getCarts();

  if (carts != null) {
    for(CartEntity cart : new ArrayList<>(carts)) {
      cart.removeProduct(product);
      cartDao.save(cart);
    }
  }

  groupDao.save(group);
}

答案 2 :(得分:0)

它将删除关联,您只需要进行一些小的调整即可。

  1. 1:N。。删除Product时,您无需执行其他任何操作即可删除与{{1}的关联},因为产品本身拥有关联(在数据库列Group中)。您只需需要提交交易。下次当您从数据库加载组时,肯定不会包含该产品。
  2. N:M 无法自动删除关联,因为它存储在单独的表中,并且您没有单独的实体。 (对于N:M关系,您不应使用product.group_id)。您要做的是在删除产品之前删除关联。只需向CascadeType.ALL添加另一种帮助方法。
Product

因此,最后,为了删除产品及其所有关联。您将需要执行以下操作:

public void removeFromCarts() {
        carts.forEach(c -> c.getProducts().remove(this));
        carts.clear();
}

* 请注意,您需要提交事务并关闭会话。因此,您不能依靠测试。在真实的应用程序中,按照我的描述进行操作即可

** N:M很棘手。例如,您最好使用 product.removeFromCarts(); productDao.deleteById(productId); // not sure why you remove by id (not pass object) 而不是Set来避免潜在的意外SQL。另外,我建议您考虑将N:M分为两个N:1和1:M,并为链接表使用专用的实体

答案 3 :(得分:0)

不确定我是否遵循。 Hibernate不会自动为您维护反向关联。您可以make it sensitive to changes on the owning side of the association,但就此为止。

关于测试失败的原因,cartDaoStub.findById(cartId)可能返回与您已经加载到持久性上下文中的CartEntity相同的副本。进行断言之前,尝试先致电entityManager.flush(),再致电entityManager.clear(),问题可能会消失。

相关问题