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
CodePudding user response:
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.
CodePudding user response:
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.