Duplicating Entities with #ManyToMany - java

I have two Entities: Meal and Product. Each Meal has a couple of products, and each Product can exist in each meal, so the relation is #ManyToMany, where Meal is a parent.
I would like to save Meal with Products, but the problem is that products are duplicating in DB.
How to archive a case where if the Product exists in DB, do not save it, but just wire with existing?
(Application parse products from external API (Nutritionix), collecting them together, and then is saving separately products, and Meal as a Parent with calculated data)
I tried to insert
if(!productService.ifExists(food.getFoodName())) productService.save(food);
into saving function and get rid of cascadeType, but when product already exists I'm getting an error:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing:
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#JsonBackReference
#ManyToMany(mappedBy = "foodList")
private Set<Meal> meals = new HashSet<>();
#Column(unique = false, nullable = false)
private String foodName;
...}
...
#Entity
public class Meal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#JsonManagedReference
#ManyToMany(cascade ={ CascadeType.PERSIST, CascadeType.MERGE})
private Set<Product> foodList = new HashSet<>();
#NaturalId
#Column(unique = true, nullable = false)
private String mealName;
...}
...
public Meal saveMeal(List<Food> foodList, String mealName){
Meal newMeal = new Meal();
newMeal.setMealName(mealName);
List<Product> productList = parseFoodToProduct(foodList);
productList.stream().forEach(y -> newMeal.getFoodList().add(y));
for(Product food : productList) {
newMeal.setNfCalories(newMeal.getNfCalories() + food.getNfCalories());
newMeal.setNfCholesterol(newMeal.getNfCholesterol() + food.getNfCholesterol());
newMeal.setNfDietaryFiber(newMeal.getNfDietaryFiber() + food.getNfDietaryFiber());
newMeal.setNfP(newMeal.getNfP() + food.getNfP());
newMeal.setNfPotassium(newMeal.getNfPotassium() + food.getNfPotassium());
newMeal.setNfProtein(newMeal.getNfProtein() + food.getNfProtein());
newMeal.setNfSaturatedFat(newMeal.getNfSaturatedFat() + food.getNfSaturatedFat());
newMeal.setNfSodium(newMeal.getNfSodium() + food.getNfSodium());
newMeal.setNfSugars(newMeal.getNfSugars() + food.getNfSugars());
newMeal.setNfTotalCarbohydrate(newMeal.getNfTotalCarbohydrate() + food.getNfTotalCarbohydrate());
newMeal.setNfTotalFat(newMeal.getNfTotalFat() + food.getNfTotalFat());
newMeal.setServingWeightGrams(newMeal.getServingWeightGrams() + food.getServingWeightGrams());
}
return mealService.save(newMeal);
}

First off, maybe call the ingredients Ingredients and not Products.
Second, the problem probably lies in the code of method parseFoodToProduct(foodList); that we cannot see, in connection with the directive
#JsonManagedReference
#ManyToMany(cascade ={ CascadeType.PERSIST, CascadeType.MERGE})
private Set<Product> foodList = new HashSet<>();
If you create new Products (xxx = new Product(...)) in parseFoodToProduct(foodList);, instead of loading them from the database, this surely will backfire.
Leave out the CascadeType, and always create/retrieve/update/store the Products on their own so they're completely independent of where they are used/referenced.

Related

How to send only a list of IDs in many-to-many spring boot JPA POST request instead of sending the full object's data

I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading

Delete with cascade in spring with JPA

I'm developing a delivery system for supermarket products.
I have Orders which contain a list of items. Each item contains a product.
I'm trying to delete a product, I'm performing a logic deletion by setting a flag in my product entity. But I have to remove the item which belongs to a product and also recalculate the price of the order and update its items.
Here's my code
Order.java
#Entity
#Table (name="Orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column (name = "id", nullable = false)
private long id;
#OneToMany( mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER)
#JsonIgnore
private List<Item> items;
private float totalPrice;
}
Item.java
#Entity
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#OneToOne( fetch = FetchType.EAGER)
#JoinColumn( name="id_order" )
private Order order;
#OneToOne( fetch = FetchType.EAGER)
#JoinColumn( name="id_product" )
private Product product;
private boolean active;
}
ProductController.java
#DeleteMapping("/remover-producto/{product_id}")
#ResponseStatus(HttpStatus.OK)
public void removerProducto(#PathVariable long product_id) {
Optional<Product> productToRemove = productService.findProduct(product_id);
// Get the product to remove
if(productToRemove.isPresent()) {
// Perform logical deletion
productToRemove.get().setActive(false);
// Update entity with flag
productService.eliminarLogico(productToRemove.get());
// get item from the product_id
Optional<Item> itemToRemove = itemService.itemWithProductId(product_id);
if(itemToRemove.isPresent()) {
// physical removal of item
orderService.removeItemFromOrderAndUpdatePrice(itemToRemove.get());
}
}
}
OrderServiceImpl.java
#Override
public void removeItemFromOrderAndUpdatePrice(Item item) {
// get order that contains the item
Optional<Order> orderToUpdate = orderRepository.findOrderWithItemId(item.getId());
if(orderToUpdate.isPresent()) {
// update total price from order
float total = orderToUpdate.get().getTotalPrice() - item.getProduct().getPrice();
orderToUpdate.get().setTotalPrice(total);
// filter to remove item from order items
List<Item> updatedItems = orderToUpdate.get().getItems()
.stream()
.filter(oldItem -> oldItem.getId() != item.getId())
.collect(Collectors.toList());
orderToUpdate.get().getItems().clear();
orderToUpdate.get().setItems(updatedItems);
// It is not updating with the new list of items.
orderRepository.save(orderToUpdate.get());
}
}
I defined the list of items in order with cascade type persist and remove. But the item is still in the database. The product is successfully deleted (logically) and the order has the correct total price.
I tried adding orphanRemoval=true but I get --> A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: com.bd.tpfinal.model.Order.items
isn't your mapping incorrect? I guess it should be a ManyToOne relationship
#Entity
public class Item {
...
#ManyToOne( fetch = FetchType.EAGER)
#JoinColumn( name="id_order" )
private Order order;
...
}
This method does the exact opposite of what u want to do.It returns a list with the item you want to remove and does not remove the item
// filter to remove item from order items
List<Item> updatedItems = orderToUpdate.get().getItems()
.stream()
.filter(oldItem -> oldItem.getId() == item.getId())
.collect(Collectors.toList());
in order to remove item from the list you have to filter and select all items whose id is not equal to item id.

Spring JPA most efficient way to load ManyToOne relationship?

I have an entity called StoreQuantity, which stores the current in stock quantity of all products/items in a store:
#Data
#Entity
#Table(name = "STORE_QUANTITY")
public class StoreQuantity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STORE_QUANTITY_ID", nullable = false)
private int storeQuantityId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", nullable = false)
private Product product;
#Column(name = "INSTORE_QUANTITY")
private int instoreQuantity;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "STORE_ID", nullable = false)
private Store store;
}
Corresponding Store entity:
#Entity
#Table(name = "store")
public class Store implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STORE_ID", nullable = false)
private int storeId;
#Column(name = "NAME", nullable = false)
private String name;
#JsonIgnore
#OneToMany(mappedBy = "store")
private List<StoreQuantity> storeQuantityList;
}
Im trying to retrieve all the quantities of products in all stores, and export as csv. I currently have thought of two ways of doing so:
Either Retrieve the entire storequantity table in one call, and for each storequantity I print as csv.
public String currentStoreQuantitiesCSV() {
List<StoreQuantity> storeQuantityList = storeQuantityRepository.findAllByOrderByStoreDesc();
for (StoreQuantity storeQuantity : storeQuantityList) {
//StoreId
csvString.append(storeQuantity.getStore().getStoreId()).append(',');
//ProductId
csvString.append(storeQuantity.getProduct().getProductId());
//Product Quantity
csvString.append(storeQuantity.getInstoreQuantity());
csvString.append(',');
}
Or I call them by store:
public String currentStoreQuantitiesCSV() {
List<Store> storeList = storeRepository.findAll();
for (Store store:storeList){
List<StoreQuantity> storeQuantityList = store.getStoreQuantityList();
for (StoreQuantity storeQuantity : storeQuantityList) {
//Store Name
csvString.append(storeQuantity.getStore().getName()).append(',');
//ProductId
csvString.append(storeQuantity.getProduct().getProductId());
//Product Quantity
csvString.append(storeQuantity.getInstoreQuantity());
csvString.append(',');
}
}
They both work, now it's just a matter of efficiency and ram utilization. I read by default JPA will eagerly load any ManyToOne relationships: Default fetch type for one-to-one, many-to-one and one-to-many in Hibernate
So does this mean if I choose option 1, there will be as many copies of store objects for every storequantity object? This will be extremely bad as I only have 20-or so stores, but thousands and thousands of storequantities, and id each of them are loaded with their own store object it will be very bad. Or will every storequantity point to the same store Objects? I'm only considering method two because that way there wouldnt be a lot of store objects in memory.
I did some testing looking at the stack memory, it seems that JPA will automatically map all ManyToOne relationships to one object. So in this case for example we have one store, and 10 storequantities that have a ManyToOne to that store. JPA will only instantiate one store object and point all 10 storequantity objects to that one store, instead of creating one store for every storequantity object. So option 1 will be the most efficient as we decrease the amount of database calls.

#ManyToMany duplicate entries

I have a problem with persisting. I have a Meal class in which is a list of Products. In Product class is a list of Meals -- #ManyToMany relation.
When I try to save it Compiler want to save every product, but then products are duplicated in my DB.
How I can indicate that the products are already there?
Here is my code
#Entity
public class Meal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#JsonManagedReference
#ManyToMany(cascade = {
CascadeType.MERGE
})
private List<Product> foodList = new ArrayList<>();
#NaturalId
#Column(unique = true, nullable = false)
private String mealName;
private Integer servingWeightGrams = 0;
private Integer servingQty = 0;
private Double nfCalories = 0d;
private Double nfTotalFat = 0d;
...
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#JsonBackReference
#ManyToMany(mappedBy = "foodList")
private List<Meal> meals = new ArrayList<>();
#Column(unique = false, nullable = false)
private String foodName;
...
#Service
public class MealManager {
MealService mealService;
ProductService productService;
#Autowired
public MealManager(MealService mealService, ProductService productService)
{
this.mealService = mealService;
this.productService = productService;
}
public Meal saveMeal(List<Food> foodList, String mealName){
Meal newMeal = new Meal();
newMeal.setMealName(mealName);
List<Product> productList = parseFoodToProduct(foodList);
productList.stream().forEach(y -> newMeal.getFoodList().add(y));
for(Product food : productList) {
newMeal.setNfCalories(newMeal.getNfCalories() + food.getNfCalories());
newMeal.setNfCholesterol(newMeal.getNfCholesterol() + food.getNfCholesterol());
newMeal.setNfDietaryFiber(newMeal.getNfDietaryFiber() + food.getNfDietaryFiber());
newMeal.setNfP(newMeal.getNfP() + food.getNfP());
newMeal.setNfPotassium(newMeal.getNfPotassium() + food.getNfPotassium());
newMeal.setNfProtein(newMeal.getNfProtein() + food.getNfProtein());
newMeal.setNfSaturatedFat(newMeal.getNfSaturatedFat() + food.getNfSaturatedFat());
newMeal.setNfSodium(newMeal.getNfSodium() + food.getNfSodium());
newMeal.setNfSugars(newMeal.getNfSugars() + food.getNfSugars());
newMeal.setNfTotalCarbohydrate(newMeal.getNfTotalCarbohydrate() + food.getNfTotalCarbohydrate());
newMeal.setNfTotalFat(newMeal.getNfTotalFat() + food.getNfTotalFat());
newMeal.setServingWeightGrams(newMeal.getServingWeightGrams() + food.getServingWeightGrams());
/*if(! productService.ifExists(food.getFoodName())) */ productService.save(food);
}
return mealService.save(newMeal);
}
You probably don't need to invoke productService.save(food) at the end of your for loop in MealManager.
You can try adding some utility methods in the Meal class, to keep the relationship in sync, as described in this article.
So, in your Meal class, you can add a method like
public void addProduct(Product product) {
foodList.add(product);
product.getMeals().add(this);
}
and call this method inside the for loop in MealManager, instead of simply calling newMeal.getFoodList().add(y).
Because you have CascadeType.MERGE defined for ManyToMany, the relationship will then be synchronized in both entities.
Other observations:
I can't see what the parseFoodToProduct method is doing, but assuming it retrieves the necessary products from the db, you should mark the saveMeal method as #Transactional
you're looping 2 times through the productList in saveMeal... once with stream().forEach() and once with the enhanced for... I'd keep only the enhanced for in this context

eclipselink cache on the object relationship

I have two entities:
#Entity
public class CustomerMainInformation {
#Id #GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
#Column(unique = true, length = 10)
private String customerNumber;
#OneToMany(mappedBy = "firstCustomerRelationship", cascade = CascadeType.ALL)
private List<CustomerRelationship> firstCustomerRelationship;
#OneToMany(mappedBy = "secondCustomerRelationship", cascade = CascadeType.ALL)
private List<CustomerRelationship> secondCustomerRelationship;
// setter & getter
}
#Entity
public class CustomerRelationship {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne
#JoinColumn(columnDefinition = "first")
private CustomerMainInformation firstCustomerRelationship;
#ManyToOne
#JoinColumn(columnDefinition = "second")
private CustomerMainInformation secondCustomerRelationship;
// setter & getter
}
I executed following code:
CustomerMainInformation customerMainInformation =manager.getReference(CustomerMainInformation.class, 1L);
System.out.println(customerMainInformation.getFirstCustomerRelationship().size());
System.out.println(customerMainInformation.getSecondCustomerRelationship().size());
CustomerMainInformation customerMainInformation2 = manager.getReference(customerMainInformation.getClass(), 2L);
CustomerRelationship customerRelationship = new CustomerRelationship();
customerRelationship.setFirstCustomerRelationship(customerMainInformation2);
customerRelationship.setSecondCustomerRelationship(customerMainInformation);
manager.persist(customerRelationship);
transaction.commit();
EntityManager manager2 = factory.createEntityManager();
CustomerMainInformation customerMainInformation3 = manager2.getReference(CustomerMainInformation.class, 1L);
System.out.println(customerMainInformation3.getFirstCustomerRelationship().size());
System.out.println(customerMainInformation3.getSecondCustomerRelationship().size());
In this code relationship size increment but because .getSecondCustomerRelationship() before called, list size not changed.
If #Cacheable(false) add to CustomerRelationship the .size() return correct list size.
You don't seem to be maintaining both sides of your relationships in the code shown. Calling setSecondCustomerRelationship Will set one side of it, and this side controls the foreign key field in the db. But your java object model is out of sync with your changes unless you also add the customerRelationship to customerMainInformation's collection. JPA does not maintain your relationships for you, so if you change one side, you are responsible for keeping the other side insync.
You can either set both sides, or force the customerMainInformation to be refreshed from the database after you commit. The first option is far more efficient.

Categories

Resources