【问题标题】:JPA 1:N relationship removing child does not remove it from parentJPA 1:N关系删除孩子不会将其从父母中删除
【发布时间】:2020-08-22 14:16:28
【问题描述】:

我有以下物品:

@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 实体。

我做错了什么?

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

【问题讨论】:

  • 实体groupcart代表删除前的状态。从数据库中重新获取实体,新获取的实体将不再具有已删除的产品。
  • 我是通过 cartDaoStub.findById(cartId) 和 groupDao.findById(groupId) 来做的,这些新获取的实体的列表中仍然有产品。
  • 这很奇怪……总之,product-entity 仍应存储在数据库中。这是正确的吗?
  • 是的,这很奇怪。删除后的实体产品不再存在于数据库中。它是否改变了我使用 H2 数据库的任何内容?
  • 它不应该......您是手动创建模式还是由休眠自动生成的?通常,连接表上应该有外键约束,因此数据库应该拒绝删除,因为数据库行被另一行引用。

标签: java hibernate jpa


【解决方案1】:

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

如果我们要删除前 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 有点棘手,所以如果我可以建议更改域,我会在这里做。 (集成测试在底部。)

我将添加一个单独的实体:CartItemProductCart

相关联
@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>

【讨论】:

  • 我相信这个答案是正确的。 findById 方法发现 Cart 和 Group 存在,因为查询返回了它们。但是在处理 ResultSet 时,它注意到这些实体已经存在于一级缓存中。因此,对象从缓存中返回,而不是重新查询 1:M 或 N:M 关系。刷新和清除应该会使您的测试按预期工作。
【解决方案2】:

当您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);
    }

如果你想remove一个Product,你只需要调用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,它也会删除与该实体关联的所有Products,即使其他CartEntitys 仍然与它们相关联。 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);

如果您需要从CartEntityremoveProduct(只会删除关系):

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

【讨论】:

    【解决方案3】:

    它会移除关联,你只需要做一些小的调整。

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

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

            product.removeFromCarts();
            productDao.deleteById(productId); // not sure why you remove by id (not pass object)
    

    *请注意,您需要提交事务并关闭会话。所以你不能依赖测试。在真正的应用程序中,当您按照我的描述进行操作时,它会起作用

    **N:M 很棘手。例如,您应该更好地使用Set 而不是List 以避免在后台出现意外的SQL。另外,我建议您考虑将 N:M 拆分为两个 N:1 和 1:M,并为链接表设置一个专用实体

    【讨论】:

      【解决方案4】:

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

      至于您的测试失败的原因,cartDaoStub.findById(cartId) 可能返回您已经加载到持久性上下文中的 CartEntity 的相同副本。在做出断言之前尝试调用entityManager.flush(),然后是entityManager.clear(),问题可能会消失。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-03-29
        • 2019-02-02
        • 2017-06-23
        • 2012-06-13
        • 1970-01-01
        • 1970-01-01
        • 2011-01-01
        相关资源
        最近更新 更多