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

如何解决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,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上,但是到目前为止还算不上成功。

解决方法

对于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,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>
,

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

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

  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,并为链接表使用专用的实体

,

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-