jpa1:n关系删除子关系不会将其从父关系中删除

mbskvtky  于 2021-07-06  发布在  Java
关注(0)|答案(4)|浏览(296)

我有以下对象:

@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中拉入group&cart时,它们的列表中仍然有产品,而从db中拉入的产品返回为null。我曾尝试为@onetomany adnotation添加“orphanremoval=true”值,但对group entity似乎不起作用。
我做错什么了?
我已经开始尝试将所有类型的cascade(除了remove)添加到productclass上的@manytone中,但到目前为止还没有成功。

ep6jt1vc

ep6jt1vc1#

当你 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);

另一方面,我们之间存在着多对多的关系 Product 以及 CartEntity .
首先,如果您配置实体 CartEntityCascade.ALL 就像你的例子:

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

它可能会产生一种不希望的效果:如果您删除 CartEntity ,它将删除所有 Product 也与实体关联,即使其他 CartEntity 仍然与它们关联。vladmihalcea在本文中对此进行了详细的解释。
为了避免这个问题,最好的选择是定义如下关系:

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

请注意 removeProduct 以及 removeProducts 方法。
使用此代码,如果需要删除 CartEntity ,只需执行以下操作:

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

如果你需要的话 removeProductCartEntity (仅删除关系):

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

如果你需要传播
Product removeCartEntity ,我认为最好的选择是创建一个处理整个流程的业务方法。你可以这样想:

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

9ceoxa922#

对于1:n,你的应该只需要稍微调整就可以了。
失败原因:执行“groupdao.save(group);”此组现在位于持久性上下文中,调用“groupdao.findbyid(groupid).get().getproducts().size()”将返回持久性上下文中的副本。
要解决这个问题,只需添加:entitymanager.flush();和entitymanager.clear();在Assert之前
我想用这个集成测试来演示一下

@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有点棘手,所以如果我能建议更改域,我会在这里做(集成测试在底部。)
我将添加一个单独的实体: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,因为后者将循环遍历每个cartime)

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>
uplii1fm

uplii1fm3#

我不知道我是否明白。hibernate不会自动为您维护反向关联。您可以使它对关联所属方的更改敏感,但仅此而已。
为什么你的测试失败了, cartDaoStub.findById(cartId) 可能返回相同的 CartEntity 已经加载到持久性上下文中的。试着打电话 entityManager.flush() 然后 entityManager.clear() 在做出Assert之前,问题很可能会消失。

r6vfmomb

r6vfmomb4#

它将删除关联,您只需要做一些小的调整。
1:不适用。当你移除 Product ,您不必执行任何其他操作来删除它与的关联 Group ,因为产品本身拥有关联(在db列中) product.group_id ). 您只需要提交事务。下一次当你从数据库加载一个组时,它肯定不会包含这个产品。
护士:m。无法自动删除关联,因为它存储在一个单独的表中,并且没有单独的实体(你不应该使用 CascadeType.ALL 对于n:m关系)。您要做的是在删除产品之前删除关联。只需将另一个助手方法添加到 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,并为链接表指定一个专用实体

相关问题