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
Related
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 have two entities, which are in a many to many relationship.
#Entity
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
private Set<Room> rooms;
}
To yield data, i have a repository for "Room" and "Team":
public interface RoomRepository extends CrudRepository<Room, Long> {
}
public interface TeamRepository extends CrudRepository<Team, Long> {
}
My goal is to request all rooms of a team, but prevent JPA from looping infinitely.
#RestController
#RequestMapping("....")
public class RoomController {
#Autowired
private RoomRepository roomRepository;
#GetMapping
public Iterable<Room> getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final var team = teamRepository.findById(exampleId);
return ResponseEntity.ok(team);
}
}
This is the result:
{
"id": 1,
"name": "Team1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
{
"id": 1,
"name": "Team 1",
"rooms": [
{
"id": 1,
"name": "Room 1",
"teams": [
Jackson will loop forever, until an exception occurs (Since the back reference also references the parent element, which will create a loop).
I already tried #JsonManagedReference and #JsonBackReference, but they are used for many to one relationships.
How do i stop Jackson from looping infinitely? I want to affect other repositories and queries as little as possible.
Your controller shoud not return entities ( classes with the annotation #Entity). As a best practice is to create another separate class with same attributes. This code has a little dupplication but it keeps all the layers clean. I also suggest to use #Service.
public class RoomDTO {
private String name;
private List<TeamDTO> teams = new ArrayList<>();
public RoomDTO() {
}
public RoomDTO(Room room) {
this.name = room.name;
for(Team team : room.getTeams()) {
TeamDTO teamDTO = new TeamDTO();
teamDTO.setName(team.getName);
teams.add(teamDTO);
}
}
}
public class TeamDTO {
List<RoomDTO> rooms = new ArrayList();
public TeamDTO() {
}
public TeamDTO(Team team) {
this.name = team.name;
for(Room room : team.getRooms()) {
RoomDTO roomDTO = new RoomDTO();
roomDTO.setName(team.getName);
rooms.add(roomDTO);
}
}
}
The controller should return this
#GetMapping
public Iterable<TeamDTO> getAllRoomsOfTeam() {
final long exampleId = 1;
final var team = teamRepository.findById(exampleId);
TeamDTO teamDTO = new TeamDTO(team);
return ResponseEntity.ok(teamDTO);
}
How to use DTOs in the Controller, Service and Repository pattern
Currently, there is cyclic dependency in your classes which is causing issues while converting objects to JSON. Please add #JsonIgnore annotation on rooms variable in your Team class as shown in below example:
import com.fasterxml.jackson.annotation.JsonIgnore;
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonIgnore
private Set<Room> rooms;
}
If you need a solution for bidirectional conversion then you can use JsonView annotation.
First of all you need to create JSON view profiles for Team and Room as shown in below example:
public class JsonViewProfiles
{
/**
* This profile will be used while converting Team object to JSON
*/
public static class Team {}
/**
* This profile will be used while converting Room object to JSON
*/
public static class Room {}
}
Use above created JSON view profiles in your entities as shown in below example:
public class Room {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({ JsonViewProfiles.Team.class, JsonViewProfiles.Room.class })
private long id;
#JsonView(JsonViewProfiles.Room.class)
#ManyToMany(mappedBy = "rooms")
private Set<Team> teams;
}
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView({JsonViewProfiles.Team.class, JsonViewProfiles.Room.class})
private long id;
#ManyToMany
#JoinTable(name = "teams_rooms",
joinColumns = #JoinColumn(name= "team_id"),
inverseJoinColumns = #JoinColumn(name = "room_id"))
#JsonView(JsonViewProfiles.Team.class)
private Set<Room> rooms;
}
While converting your object to JSON please use these profiles as shown in below example:
#GetMapping
public String getAllRoomsOfTeam() {
final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
final Team team = teamRepository.findById(exampleId);
String result = new ObjectMapper().writerWithView(JsonViewProfiles.Team.class)
.writeValueAsString(team);
return result;
}
I have two entity that have a relation,The relationship works fine, but how can I set value from one object to another in controller.
#Entity
#Table(name = "material_manu_calculator")
public class MaterialManuCalcu {
#Id
#GeneratedValue
#Column(name = "no")
private int no;
#ManyToOne
#JoinColumn(name = "order_id")
private OrderProductManu orderProductManu;
//.....getters and setters and constructors}
Below is the second Entity
#Entity
#Table(name = "orders_products_manu")
public class OrderProductManu {
#Id
#GeneratedValue
#Column(name = "order_id")
private int orderManuId;
#OneToMany(cascade = CascadeType.ALL,mappedBy = "orderProductManu")
private List<MaterialManuCalcu> materialCalcu = new ArrayList<>();
//.....getters and setters and constructors}
below is the Repository
#Repository
#Transactional
public interface OrderProductManuRepository extends JpaRepository <OrderProductManu, Integer> {
#Query(value ="SELECT *FROM orders_products_manu WHERE orders_products_manu.order_id =?", nativeQuery = true)
public OrderProductManu getOrderProductById(int id);
}
I want to set the value of MaterilaManuCalcu in controller as below
#Controller
public class ProductsController {
#Autowired
private OrderProductManuRepository orderRepo;
OrderProductManu orderProduct = orderRepo.getOrderProductById(1);
MaterialManuCalcu manCalc = new MaterialManuCalcu();
manCalc.setOrderProductManu(orderProduct.getOrderManuId());
// I get the error says:
// The method setOrderProductManu(OrderProductManu) in
// the type MaterialManuCalcu is not applicable for the arguments (int)
Update: Constructors
public MaterialManuCalcu(int no, int amountOrdered, int amountAvailable, int amountWillRemain,
MaterialManu materialmanu, OrderProductManu orderProductManu) {
this.no = no;
this.amountOrdered = amountOrdered;
this.amountAvailable = amountAvailable;
this.amountWillRemain = amountWillRemain;
this.materialmanu = materialmanu;
this.orderProductManu = orderProductManu;
}
Another one
public OrderProductManu(int orderManuId, String customerName, int amountOrderedManu, String dateOrdered, Users users,
ProductsManu productsManu) {
this.orderManuId = orderManuId;
this.customerName = customerName;
this.amountOrderedManu = amountOrderedManu;
this.dateOrdered = dateOrdered;
this.users = users;
this.productsManu = productsManu;
}
Update:Showing how both entities are created
For : OrderProductManu
OrderProductManu orderProduct = new OrderProductManu();
orderProduct.setDateOrdered("2021-04-14");
orderProduct.setAmountOrderedManu(platenum);
orderProduct.setCustomerName("Wapili Mteja");
orderProduct.setUsers(userMoja.get(0));
orderProduct.setProductsManu(typeofProduct);
orderProductManus.setOrderManuId(007);//this is the value that I want to set inside
//MateriaManCalcu entity for property setOrderProductManu
//You can check the relationship above
For: MaterialManuCalcu
MaterialManuCalcu manCalc = new MaterialManuCalcu();
manCalc.setAmountAvailable(availableSheets);
manCalc.setAmountOrdered(sheetsNum);
manCalc.setAmountWillRemain(sheetWillRemain);
manCalc.setMaterialmanu(materialSheet);
manCalc.setOrderProductManu(orderProduct);//doing this the whole object of
//orderProduct entity goes inside a one column in our MatrialManuCalcuof entity
Table:material_manu_calculator
How should I do this correctly. Thanks in advance.
You are trying to set id of orderProduct which is returned by calling orderProduct.getOrderManuId() of type int to variable of type OrderProductManu.
Just pass your orderProduct like this manCalc.setOrderProductManu(orderProduct)
Here is an example code demonstrating the issue:
The Meal Entity:
#Entity
public class Meal {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(mappedBy = "meal", cascade = CascadeType.PERSIST)
private Collection<Food> foods;
public Meal() {
foods = new HashSet<>();
}
public Collection<Food> getFoods() {
return foods;
}
public void addFood(Food food) {
foods.add(food);
// without this the `meal_id` column is null
food.setMeal(this);
}
}
The Food Entity:
#Entity
public class Food {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne
private Meal meal;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Meal getMeal() {
return meal;
}
public void setMeal(Meal meal) {
this.meal = meal;
}
}
And here is the code that creates and saves the entities:
Meal meal = new Meal();
for (int i = 0; i < 10; i++) {
meal.addFood(new Food());
}
mealRepository.save(meal);
Both the Meal and the Food entities are persisted thanks to the CascadeType.PERSIST, but the meal_id column stays null if I don't explicitly set the meal field to the Meal entity.
This is not the behavior I'd expect, and I'm wondering why isn't this automatically done for me.
#Entity
public class Meal {
#OneToMany(mappedBy = "meal", cascade = CascadeType.PERSIST)
private Collection<Food> foods;
}
The mappedBy here means that the value of meal field in Food is used to provide the value for the corresponding database column that link between Food and Meal (i.e. meal_id column in Food table). So if you do not set it, it will always remain NULL.
On the other hand, without mappedBy means that the value of the foods field in Meal is used to provide the value for meal_id column. The reason of manually syncing them is just to have a consistent OOP model to work with.
CascadeType.PERSIST is nothing to do with setting the value of the entity. It will never update the value of the entities for you. What it helps is to automatically calling some entityManager methods on the related entities. Without it, you have to call the following to save all Food and Meal:
entityManager.persist(meal);
entityManager.persist(food1);
entityManager.persist(food2);
......
entityManager.persist(foodN);
With it , you just need to call
entityManager.persist(meal);
and the rest of entityManager.persist(foodN) will be 'cascaded' to call automatically.
So in term of spring-data-jpa , instead of calling
mealRepository.save(meal);
foodRepository.persist(food1);
foodRepository.persist(food2);
......
foodRepository.persist(foodN);
You just need to call
mealRepository.save(meal);
Note: for simplyfication i have changed some variables names and get rid of unnecessary code to show my issue.
I have two repositories:
#Repository
public interface CFolderRepository extends CrudRepository<CFolder, Long>, QuerydslPredicateExecutor<CFolder> {}
#Repository
public interface CRepository extends JpaRepository<C, Long>, CFinder, QuerydslPredicateExecutor<C> {}
The class C is:
#FilterDef(name = "INS_COMPANY_FILTER", parameters = {#ParamDef(name = "insCompanies", type = "string")})
#Filter(name = "INS_COMPANY_FILTER", condition = " INS_COMPANY in (:insCompanies) ")
#NoArgsConstructor
#AllArgsConstructor
#Audited
#AuditOverrides({#AuditOverride(forClass = EntityLog.class),
#AuditOverride(forClass = MultitenantEntityBase.class)})
#Entity
#Table(name = "INS_C")
#Getter
public class C extends MultitenantEntityBase {
#OneToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "C_FOLDER_ID")
private CFolder cFolder;
public void addFolder(List<String> clsUrl){
this.cFolder = CFolder.createFolder(clsUrl);
}
}
CFolder is:
#Getter
#NoArgsConstructor
#Audited
#AuditOverride(forClass = EntityLog.class)
#Entity
#Table(name = "C_FOLDER")
#AllArgsConstructor
public class CFolder extends EntityBase {
#Column(name = "CREATION_FOLDER_DATE_TIME", nullable = false)
private LocalDateTime creationFolderDateTime;
#Column(name = "UPDATED_FOLDER_DATE_TIME")
private LocalDateTime updatedFolderDateTime;
#Column(name = "FOLDER_CREATED_BY", nullable = false)
private String folderCreatedBy;
#Column(name = "FOLDER_UPDATED_BY")
private String folderUpdatedBy;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "cFolder", fetch = FetchType.EAGER)
#NotAudited
private Set<FolderDocument> folderDocuments = new HashSet<>();
public static CFolder createFolder(List<String> clsUrl){
CFolder cFolder = new CFolder(LocalDateTime.now(), null, SecurityHelper.getUsernameOfAuthenticatedUser(), null, new HashSet<>());
createFolderDocuments(clsUrl, cFolder);
return cFolder;
}
public void updateFolder(List<String> clsUrl){
this.updatedFolderDateTime = LocalDateTime.now();
this.folderUpdatedBy = SecurityHelper.getUsernameOfAuthenticatedUser();
this.folderDocuments.clear();
createFolderDocuments(clsUrl, this);
}
private static void createFolderDocuments(List<String> clsUrl, CFolder cFolder) {
int documentNumber = 0;
for (String url : clsUrl) {
documentNumber++;
cFolder.folderDocuments.add(new FolderDocument(cFolder, documentNumber, url));
}
}
}
FolderDocument is:
#Getter
#NoArgsConstructor
#AllArgsConstructor
#Audited
#AuditOverride(forClass = EntityLog.class)
#Entity
#Table(name = "FOLDER_DOCUMENT")
public class FolderDocument extends EntityBase {
#ManyToOne
#JoinColumn(name = "C_FOLDER_ID", nullable = false)
private CFolder cFolder;
#Column(name = "DOCUMENT_NUMBER", nullable = false)
private int documentNumber;
#Column(name = "URL", nullable = false)
private String url;
}
And finally we have a service in which i use these entities and try to save/load them to/from database:
#Service
#AllArgsConstructor(onConstructor = #__(#Autowired))
public class CFolderService {
private final CRepository cRepository;
private final CommunicationClServiceClient communicationServiceClient;
private final CFolderRepository cFolderRepository;
public List<ClDocumentDto> getClCaseFolder(Long cId) {
C insCase = cRepository.findCById(cId);
List<ClDocumentDto> clDocumentsDto = getClDocuments(insCase.getCNumber()); // here, the object has one cFolder, but many FolderDocument inside of it
return clDocumentsDto;
}
#Transactional
public void updateCFolder(Long cId) {
C insC = cRepository.findCById(cId);
List<ClDocumentDto> clDocumentsDto = getClDocuments(insC.getCNumber());
List<String> clsUrl = clDocumentsDto.stream().filter(c -> "ACTIVE".equals(c.getCommunicationStatus())).map(ClDocumentDto::getUrl).collect(Collectors.toList());
if (Objects.isNull(insC.getCFolder())) {
insC.addFolder(clsUrl);
} else {
insC.getCFolder().updateFolder(clsUrl);
}
cFolderRepository.save(insC.getCFolder()); // here it saves additional FolderDocument instead of updateing it
cRepository.save(insC); // need second save, so can get these collection in getClaimCaseFolder successfully
}
}
I have two issues inside. In the example i was trying to clear the objects that i found from DataBase and create new ones.
1)
First is that i have to make two save operation to successfully restore the object in getClCaseFolder method (outside transactional).
2)
Second is that everytime i am saving - i get additional FolderDocument object pinned to CFolder object inside C object. I want to clear this collection and save new one.
I am not sure why hibernate does not update this object?
EDIT:
I think that i do sth like:
cRepository.save(insC);
instead of this.folderDocuments.clear();
i can do:
for(Iterator<FolderDocument> featureIterator = this.folderDocuments.iterator();
featureIterator.hasNext(); ) {
FolderDocument feature = featureIterator .next();
feature.setCFolder(null);
featureIterator.remove();
}
But i get eager fetching, why lazy wont work? There is an error using it.
Check whether you are setting ID in that Entity or not.
If ID is present/set in entity and that ID is also present in DB table then hibernate will update that record, But if ID is not present/set in Entity object the Hibernate always treat that object as a new record and add new record to the table instead of Updating.