【问题标题】:Spring JPA: How to insert/update new item with unique column when using Many to Many relationshipSpring JPA:使用多对多关系时如何插入/更新具有唯一列的新项目
【发布时间】:2021-11-01 07:34:25
【问题描述】:

我想创建一个带有类别的新产品。如果该类别不存在,则还要创建该类别。到目前为止,这个用例很好。

当我想创建另一个具有相同类别的产品时,我的第二个用例出现了。由于我的类别实体具有唯一名称,因此当我调用我的服务时,它会抛出重复的错误键。我想要的是能够创建第二个产品并更新类别以拥有该产品。

这是我的类别实体

package com.smolano.cupboard.entities;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Category implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String name;
    @ManyToMany(mappedBy = "categories", fetch = FetchType.LAZY)
    private Set<Product> products = new HashSet<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Product> getProducts() {
        return products;
    }

    public void setProducts(Set<Product> products) {
        this.products = products;
    }
}

这是我的产品实体

package com.smolano.cupboard.entities;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Product implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(
            name = "product_categories",
            joinColumns = {@JoinColumn(name = "product_id")},
            inverseJoinColumns = {@JoinColumn(name = "category_id")}
    )
    private Set<Category> categories = new HashSet<>();
    private String barCode;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<Category> getCategories() {
        return categories;
    }

    public void setCategories(Set<Category> categories) {
        this.categories = categories;
    }

    public String getBarCode() {
        return barCode;
    }

    public void setBarCode(String barCode) {
        this.barCode = barCode;
    }
}

我的分类库

package com.smolano.cupboard.repositories;

import com.smolano.cupboard.entities.Category;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoryRepository extends JpaRepository<Category, Long> {
}

和我的产品存储库

package com.smolano.cupboard.repositories;

import com.smolano.cupboard.entities.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

我的其余代码可以在这里找到 https://github.com/santfirax/backend-product-store-java

【问题讨论】:

  • 我尝试了一些我在 SO 上看到的解决方案,但它不起作用。一种是实现 hascode 和 equals 方法,但调用 GET 方法时会导致无限递归。

标签: java spring spring-boot hibernate jpa


【解决方案1】:

在这个例子中,我认为没有必要建立多对多关系。一对多可以达到目的,其中一个类别可以有多个产品,您可以使用名称唯一的同一类别创建多个产品。

如果您仍然想实现它,您需要有一个新表,其中包含两个表的主键组合,这将是唯一的组合。 请参阅这些页面以获取更多信息 12 以及视觉表示。

【讨论】:

    【解决方案2】:

    尽量避免将CascadeType.REMOVE 用于多对多关联,因为它可能会生成过多的查询并删除比您预期更多的记录。 Here 是一篇详细解释此行为的文章。由于您在映射中使用CascadeType.ALL,因此它还包括CascadeType.REMOVE

    关于向产品实体添加/更新新类别。覆盖 equalshashCode 方法会很有帮助。由于在您的示例中 Category.name 应该是唯一的,并且基本上您可以通过名称来区分类别,因此您的实现可能如下所示:

        @Override
        public boolean equals(Object o) {
            if (o == this)
                return true;
            if (!(o instanceof Category))
                return false;
            Category other = (Category) o;
            return (this.name == null && other.name == null) || (this.name != null && this.name.equals(other.name));
        }
    
        @Override
        public int hashCode() {
            return this.name.hashCode();
        }
    

    这样您将确保您的 Product.categories 字段不会包含同名的类别。

    假设您的实体映射具有cascade = {CascadeType.PERSIST, CascadeType.MERGE},您的服务类可能类似于以下内容:

    @Service
    @Transactional
    public class ProductService {
        @Autowired
        private ProductRepository productRepository;
    
        public Product createProduct(Product product) {
            return productRepository.save(product);
        }
    
        public Product saveCategoryInProduct(Long productId, Category category) {
            return productRepository
                    .findById(productId)
                    .map(product -> addCategory(product, category))
                    .map(productRepository::save)
                    .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        }
    
        private Product addCategory(Product product, Category category) {
            product.getCategories().add(category);
            return product;
        }
    }
    

    通过调用saveCategoryInProduct,您将向产品实体添加新类别。如果 Category 实体包含 id(非空),则将在 product_categories 表中创建新记录。

    但是如果Category实体没有id(为空),那么将首先尝试在category表中创建新记录,然后将其关联到提供的产品id(通过创建记录在product_categories)。在这种情况下,您可能会遇到DuplicateKeyException。要处理它,您可以通过以下方式修改addCategory 方法:

        private Product addCategory(Product product, Category category) {
            product.getCategories().add(
                    categoryRepository
                            .findByName(category)
                            .orElse(category));
            return product;
        }
    

    这样您将确保您添加的类别要么是从 db 中检索到的(因此不会再次创建),要么尚不存在。

    【讨论】:

    • 仍然不确定使用您的两个新方法 createProduct 和 saveCategoryInProduct。我应该两个都打电话吗?还是会?
    • 在 Category 中仅添加 hascode 和 equals 方法不起作用
    • 要测试它,只需首先调用createProduct(它将在数据库中创建新的Product记录,没有任何关联的类别),然后使用新创建的产品的id调用saveCategoryInProduct,例如:产品产品 = productService.createProduct(new Product(...)); productService.saveCategoryInProduct(product.getId, new Category(...))`
    • 覆盖 hashCodeequals 可能会间接帮助。下面是一个示例:假设您检索了一个带有类别的产品(每个类别将有 idname),然后您决定创建新类别并将其添加到 Set&lt;Category&gt; 具有相同名称但没有 ID(无效的)。在不覆盖 hashCode 的情况下,可能在 Set 字段中有 2 个具有相同名称的类别,通常您不希望发生这种情况,因为在保存 Product 实体时可能会导致 DuplicateKeyException。可能这个例子并不完美,但我很确定可能还有其他情况。
    • 我按照你的建议做了,但错误仍然出现。当我尝试创建具有相同类别的另一个产品时,它会引发类别重复键的错误。
    猜你喜欢
    • 1970-01-01
    • 2016-01-03
    • 1970-01-01
    • 1970-01-01
    • 2020-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-24
    相关资源
    最近更新 更多