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.
Related
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
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.
I am currently stuck on an issue of updating a parent entity field and getting the update to cascade to all its children's fields.
Here is an example of what I am trying to accomplish:
User.java
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, columnDefinition = "TINYINT(1) default false")
private Boolean archived = Boolean.FALSE;
#OneToMany(mappedBy = "user")
private Set<Invoice> invoices = new HashSet<Invoice>();
// Setters & Getters
}
Invoice.java
#Entity
public class Invoice{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, columnDefinition = "TINYINT(1) default false")
private Boolean archived = Boolean.FALSE;
#ManyToOne(mappedBy = "invoices")
#JoinColumn(nullable = false)
private User user;
// Setters & Getters
}
When I update the archived value to true. I want all the invoices to also be updated to true as well.
I.E
public Boolean archiveUserById(Integer id) {
User user= entity_manager.find(User.class, id);
Boolean result = false;
if(auction != null) {
// This should cascade to all the invoices as well and update their archived fields to true as well
user.setArchived(true);
try {
entity_manager.getTransaction().begin();
entity_manager.merge(auction);
entity_manager.getTransaction().commit();
result = true;
} catch(Exception e) {
e.printStackTrace();
}
}
return result;
}
I've tried using cascade = CascadeType.PERSIST and #JoinTable(....) with all the referenced columns, but they are still failing to update the fields correctly.
To clarify is there a way to update a child's field through its parents' update with a Cascade effect?
Thank-you for the help.
EDIT
To clarify my question, I am trying to add a constraint cascade effect when a field on the parent entity is updated to reflect on the child entity's same field. I am trying to avoid any logic within the Entity itself. Is there a way to do this through annotations only?
Something to the same effect as this:
ALTER TABLE `child` ADD CONSTRAINT `childs-archived-mirrors-parent`
FOREIGN KEY (`archived`, `parentId`)
REFERENCES `parent`(`archived`, `id`)
ON DELETE RESTRICT ON UPDATE CASCADE;
try adding #PreUpdate method on parent where you manually change children inside the list.
Thanks for all your help.
I ended up finding a solution. I created a custom foreign key definition that will cascade any update on the archived field to its child entities.
Below is how I accomplished that.
#Entity
public class Invoice{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(nullable = false, columnDefinition = "TINYINT(1) default false")
private Boolean archived = Boolean.FALSE;
#ManyToOne(mappedBy = "invoices")
#JoinColumn(nullable = false, foreignKey = #ForeignKey(foreignKeyDefinition = "FOREIGN KEY (archived, user_id) REFERENCES user(archived, id) ON DELETE RESTRICT ON UPDATE CASCADE"))
private User user;
// Setters & Getters
}
According to this post Difference between #OneToMany and #ElementCollection? I should prefer #ElementCollection for embeddable types and #OneToMany for entities. But using #OneToMany I can additionaly set option orphanRemoval=true. How can I do this with #ElementCollection? It it implied?
It is implied. Removing the owning entity would also remove all data on the #ElementCollection. Setting the Collection to null or changing elements in the Collection would cause an update if Session isn't already closed.
The official documentation here says this:
2.8.1. Collections as a value type
Value and embeddable type collections have a similar behavior as
simple value types because they are automatically persisted when
referenced by a persistent object and automatically deleted when
unreferenced. If a collection is passed from one persistent object to
another, its elements might be moved from one table to another.
...
For collections of value types, JPA 2.0 defines the #ElementCollection
annotation. The lifecycle of the value-type collection is entirely
controlled by its owning entity.
I ran these three tests to test it out:
#Test
public void selectStudentAndSetBooksCollectionToNull() {
Student student = studentDao.getById(3L);
List<String> books = student.getBooks();
books.forEach(System.out::println);
student.setBooks(null);
em.flush(); // delete from student_book where student_id = ?
}
#Test
public void selectStudentAndAddBookInCollection() {
Student student = studentDao.getById(3L);
List<String> books = student.getBooks();
books.add("PHP Book");
books.forEach(System.out::println);
em.flush(); // insert into student_book(student_id, book) values(?, ?)
}
#Test
public void selectStudentAndChangeCollection() {
Student student = studentDao.getById(3L);
List<String> newBooks = new ArrayList<>();
newBooks.add("Rocket Engineering");
newBooks.forEach(System.out::println);
student.setBooks(newBooks);
em.flush(); // delete from student_book where student_id = ?
// insert into student_book(student_id, book) values(?, ?)
}
This is the Student class:
#Entity
#Table(name = "student")
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "student_id", nullable = false, insertable = false, updatable = false)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#ElementCollection
#CollectionTable(
name = "student_books",
joinColumns = #JoinColumn(name = "student_id", referencedColumnName = "student_id"))
#Column(name = "book")
private List<String> books = new ArrayList<>();
// Getters & Setters
}
I'm having trouble with a JPA/Hibernate (3.5.3) setup, where I have an entity, an "Account" class, which has a list of child entities, "Contact" instances. I'm trying to be able to add/remove instances of Contact into a List<Contact> property of Account.
Adding a new instance into the set and calling saveOrUpdate(account) persists everything lovely. If I then choose to remove the contact from the list and again call saveOrUpdate, the SQL Hibernate seems to produce involves setting the account_id column to null, which violates a database constraint.
What am I doing wrong?
The code below is clearly a simplified abstract but I think it covers the problem as I'm seeing the same results in different code, which really is about this simple.
SQL:
CREATE TABLE account ( INT account_id );
CREATE TABLE contact ( INT contact_id, INT account_id REFERENCES account (account_id) );
Java:
#Entity
class Account {
#Id
#Column
public Long id;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "account_id")
public List<Contact> contacts;
}
#Entity
class Contact {
#Id
#Column
public Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
public Account account;
}
Account account = new Account();
Contact contact = new Contact();
account.contacts.add(contact);
saveOrUpdate(account);
// some time later, like another servlet request....
account.contacts.remove(contact);
saveOrUpdate(account);
Result:
UPDATE contact SET account_id = null WHERE contact_id = ?
Edit #1:
It might be that this is actually a bug
http://opensource.atlassian.com/projects/hibernate/browse/HHH-5091
Edit #2:
I've got a solution that seems to work, but involves using the Hibernate API
class Account {
#SuppressWarnings("deprecation")
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account")
#Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
#JoinColumn(name = "account_id", nullable = false)
private Set<Contact> contacts = new HashSet<Contact>();
}
class Contact {
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
private Account account;
}
Since Hibernate CascadeType.DELETE_ORPHAN is deprecated, I'm having to assume that it has been superseded by the JPA2 version, but the implementation is lacking something.
Some remarks:
Since you have a bi-directional association, you need to add a mappedBy attribute to declare the owning side of the association.
Also don't forget that you need to manage both sides of the link when working with bi-directional associations and I suggest to use defensive methods for this (shown below).
And you must implement equals and hashCode on Contact.
So, in Account, modify the mapping like this:
#Entity
public class Account {
#Id #GeneratedValue
public Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true)
public List<Contact> contacts = new ArrayList<Contact>();
public void addToContacts(Contact contact) {
this.contacts.add(contact);
contact.setAccount(this);
}
public void removeFromContacts(Contact contact) {
this.contacts.remove(contact);
contact.setAccount(null);
}
// getters, setters
}
In Contact, the important part is that the #ManyToOne field should have the optional flag set to false:
#Entity
public class Contact {
#Id #GeneratedValue
public Long id;
#ManyToOne(optional = false)
public Account account;
// getters, setters, equals, hashCode
}
With these modifications, the following just works:
Account account = new Account();
Contact contact = new Contact();
account.addToContact(contact);
em.persist(account);
em.flush();
assertNotNull(account.getId());
assertNotNull(account.getContacts().get(0).getId());
assertEquals(1, account.getContacts().size());
account.removeFromContact(contact);
em.merge(account);
em.flush();
assertEquals(0, account.getContacts().size());
And the orphaned Contact gets deleted, as expected. Tested with Hibernate 3.5.3-Final.