简体   繁体   中英

Spring JPA: How to insert/update new item with unique column when using Many to Many relationship

I would like to create a new Product with a category. If the category doesn't exist then also create the category. So far so good with this use case.

My second use case comes when I want to create another product with the same category. Since I have my Category entity with unique name then when I call my service it throws the duplicate error key. What I want is to be able to create the second product and update the category to have this product.

This is my Category Entity

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

This is my Product Entity

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

My category repository

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> {
}

and my product repository

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> {
}

Rest of my code can be found here https://github.com/santfirax/backend-product-store-java

In this example, I don't see a necessity of having many to many relationship. one to many can serve the purpose, where one category can have multiple products and you can create multiple products using the same category which is unique by name.

If you still want to achieve it, you need to have a new table with the combination of primary keys of both tables and that is going to be the unique combination. refer these pages for more info 1 and 2 with visual representation.

Try to avoid using CascadeType.REMOVE for many-to-many associations, since it may generate too many queries and remove more records than you expected. Here is an article that explains this behavior in details. Since you're using CascadeType.ALL in your mapping it also includes CascadeType.REMOVE .

Regarding adding/updating new categories to product entity. It would be helpful to override equals and hashCode methods. Since in your example Category.name should be unique and basically you distinguish categories by their names, then your implementations may look like this:

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

This way you'll make sure that your Product.categories field will not contain categories with the same name.

Assuming that your entity mapping have cascade = {CascadeType.PERSIST, CascadeType.MERGE} , your service class may look similar to following:

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

By calling saveCategoryInProduct , you'll add new categories to the product entities. If Category entity contains id (not null) then new record will be created in product_categories table.

However if Category entity does not have id ( is null), then there will be an attempt to create new record in category table first, then associate it to provided product id (by creating record in product_categories ). In this case you may encounter DuplicateKeyException . To deal with it you may modify addCategory method in a following way:

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

This way you'll make sure that the category you add either retrieved from db(therefore will not be created again) or does not exists yet.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM