Spring Data JPA problem with updating multiple Entities - java

I have 2 Entity classes the "Menu" which only has one field called "name" and second Entity - "Ingredients" which has 2 fields - "ingredientName" and "ingredientDescription". Database Structure
I'm creating a simple CRUD web-app , but the update method instead of updating the Entity , it inserts new values in the DB. I checked and when user clicks on the update on specified menu, the first entity's id and its ingredients id's as well are predifined. Im new to spring boot and thymeleaf and Don't really know how to work with JPA when you have more than 1 entity.
Menu entity :
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
#Column(name = "name")
private String name;
// Mapping To second table
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinTable(name = "menu_ingredient",
joinColumns = #JoinColumn(name = "menu_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_id"))
private List<Ingredients> ingredient = new ArrayList<>();
//Getters/Setters/Constructors/ToString
Ingredients entity :
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name = "ingredient")
private String ingredientName;
#Column(name = "description")
private String ingredientDescription;
//Getters/Setters/Constructors/ToString
Controller(Only the update methods) :
#GetMapping("/edit/{id}")
public String edit(#PathVariable(name = "id")String id, Model model){
Optional<Menu> menu = menuRepository.findById(id);
List<Ingredients> ingredients = menu.get().getIngredient();
for (Ingredients ing : ingredients){
System.out.println(ing);
}
model.addAttribute("ingredients", ingredients);
model.addAttribute("newMenu",menu);
return "edit-page";
}
#PostMapping("/postEditMenu")
public String postEdit(#ModelAttribute("newMenu")Menu menu){
menuRepository.save(menu);
return "redirect:/recipeList";
}
edit-page.html :
<form action = "#" th:action="#{/postEditMenu}" th:object="${newMenu}" method="post">
<p>Menu Name: <br><input type="text" th:field="*{name}"></p>
<div id="wrapper" th:each="ing: ${ingredients}">
<label for="ingredientName"></label>
<p>Ingredient Name: <br><input th:value="${ing.ingredientName}" id="ingredientName" type="text" name="ingName"></p>
<label for="ingredientDescription"></label>
<p>Ingredient Description:</p> <textarea id="ingredientDescription" type="text" th:text="${ing.ingredientDescription}" name="ingDesc"></textarea>
</div>
<br>
<input type="button" id="more_fields" onclick="add_fields();" value="Add More" />
<br>
<input type="submit" th:value="Submit">
</form>
FIX I actually figured it out with the help of below answers. Here's the code :
#PostMapping("/postEditMenu")
public String postEdit(#ModelAttribute("newMenu")Menu menu,
#RequestParam String ingName,
#RequestParam String ingDesc){
String[] ingNameSplit = ingName.split(",");
String[] ingDescSplit = ingDesc.split(",");
Menu menuToUpdate = menuRepository.getOne(menu.getId());
List<Ingredients> newIngredientList = menuToUpdate.getIngredient();
newIngredientList.clear();
for(int a = 0, b = 0; a<ingNameSplit.length; b++, a++){
newIngredientList.add(new Ingredients(ingNameSplit[a], ingDescSplit[b]));
}
menuToUpdate.setIngredient(newIngredientList);
menuRepository.save(menuToUpdate);
return "redirect:/recipeList";
}
So, First I added hidden "id" fields to each of the items required , like this :
<input type="text" th:field = "*{id}" hidden>
and
<input type="text" th:value = "${ing.id}" hidden>
Then, in the postEditMenu method, I added #RequestParam String ingName, and #RequestParam String ingDesc to get the input of new items from thymeleaf, then I split that String and add it to String[] array with String[] ingNameSplit = ingName.split(",") Because the input would be one big comma separated String and not array[] . Then I get the menu which user wants to update - Menu menuToUpdate = menuRepository.getOne(menu.getId()); The menu.getId() isn't null because I set hidden "id" fields in thymeleaf. Then I get the Ingredients of this menu - List<Ingredients> newIngredientList = menuToUpdate.getIngredient(); because the list would already be filled with existed ingredients I clear that list and add new ingredients which user will fill the form with -
for(int a = 0, b = 0; a<ingNameSplit.length; b++, a++){
newIngredientList.add(new Ingredients(ingNameSplit[a], ingDescSplit[b]));
}
after that I set this newIngredientsList and save the menu itself to the db -
menuToUpdate.setIngredient(newIngredientList);
menuRepository.save(menuToUpdate);
Thanks for all the help guys :)

At this point:
#PostMapping("/postEditMenu")
public String postEdit(#ModelAttribute("newMenu")Menu menu){
menuRepository.save(menu);
return "redirect:/recipeList";
}
You receive menu from edit-page.html and it has no id, that is why it always creates new records in database.
To edit the desired menu, you would need to have it's id before.
You can create endpoint for obtaining list of menus and display them with edit button next to each menu in html site. Then if user clicks edit button redirect him to your edit form, but this time you can pass menu's id.

First you need to fetch Menu entity from database by id using getOne and then you can update it.
Edit your code in postEdit method as follows:
Fetch Menu entity:
Menu menuToUpdate = menuRepository.getOne(menu.getId());
Update attributes:
menuToUpdate.setName(menu.getName());
Save entity:
menuRepository.save(menuToUpdate);

Add a hidden id field to hold the menu's id, and add hidden id fields for each ingredient.

Related

List all data from table by date

I'm curious to find a solution for this but couldn't find anything relatable and useful so far.
I have a table Transaction:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "transaction_id")
private Long id;
#Column(name = "user_id", nullable = false)
private Long userId;
#Column(name = "wallet_name", nullable = false)
private String walletName;
#NotNull(message = "Please, insert a amount")
#Min(value = 0, message = "Please, insert a positive amount")
private Double amount;
private String note;
#DateTimeFormat(pattern = "yyyy-MM-dd")
#Column(name = "date")
private LocalDate date;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "wallet_id", nullable = false)
private Wallet wallet;
#Enumerated(EnumType.STRING)
#Column(name = "transaction_type", columnDefinition = "ENUM('EXPENSE', 'INCOME')")
private TransactionType transactionType;
#Nullable
#Enumerated(EnumType.STRING)
#Column(name = "expense_categories", columnDefinition = "ENUM('FOOD_AND_DRINK', 'SHOPPING', 'TRANSPORT', 'HOME'," +
" 'BILLS_AND_FEES', 'ENTERTAINMENT', 'CAR', 'TRAVEL', 'FAMILY_AND_PERSONAL', 'HEALTHCARE'," +
" 'EDUCATION', 'GROCERIES', 'GIFTS', 'BEAUTY', 'WORK', 'SPORTS_AND_HOBBIES', 'OTHER')")
private ExpenseCategories expenseCategories;
#Nullable
#Enumerated(EnumType.STRING)
#Column(name = "income_categories", columnDefinition = "ENUM('SALARY', 'BUSINESS', 'GIFTS', 'EXTRA_INCOME', 'LOAN', 'PARENTAL_LEAVE', 'INSURANCE_PAYOUT', 'OTHER')")
private IncomeCategories incomeCategories;
Now, to display that data on Thymeleaf I created controller with model that I'm passing on Thymeleaf .
And that looks like this:
#GetMapping("/userTransactions/{user_id}")
public String getUserTransactions(#PathVariable("user_id") long user_id, Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
long userId = user.getId();
model.addAttribute("userId", userId);
model.addAttribute("transactions", transactionService.findDistinctIdByUserId(user_id));
return "transactions";
}
As you can see, Transaction is connected with User so user actually can create a Transaction, and on that controller I'm just getting all transaction from each User and this is how I display it on Thymeleaf:
<div th:each="transactions : ${transactions}">
<h2>Amount: <span th:text="${transactions.amount}"></span></h2>
<br>
<h2>Note: <span th:text="${transactions.note}"></span></h2>
<br>
<h2>Date: <span th:text="${transactions.date}"></span></h2>
<br>
<h2>Wallet name: <span th:text="${transactions.walletName}"></span></h2>
<br>
<h2>Expense Category: <span th:text="${transactions.expenseCategories}"></span></h2>
<br>
<h2>IncomeCategory: <span th:text="${transactions.incomeCategories}"></span></h2>
</div>
Don't mind the way I formatted it, point is that works fine so far, I got all data displayed on page but I want to achieve one thing:
As you can see, while creating transaction user also pick a date, and what I want?
Lets say user have two transactions for today, I mean 18/01/2023
I want to have a heading for example like <div and to pass today date and each transaction from today, and below down for example 15/01/2023 because user also have a transactions on that date.
This is an example, as you can see I have today section because user made a transaction on today date, also for yesterday and then back on January 14, so I want to separate transactions for each that like that.
Example
Sorry for SS, I tried to add code snippet but cant find option on this updated SO.
I don't know from where to start, I found some options like to sort from ascending and descending order date but I want to avoid something like that and to give my best to achieve this. So any resource, tutorial or any help here will be fine. Thanks.
If you want to break it up by date, you should do that in the Java beforehand. Instead of passing a List<Transaction>, you should pass a List<TransactionGroup> where a TransactionGroup contains all the transactions for a day. It might look something like:
class TransactionGroup {
private LocalDate date;
private List<Transaction> transactions;
/* Getters and setters */
}
Then your html becomes simple.
<div th:each="group : ${transactionGroup}">
<h1 th:text="${group.date}" />
<div th:each="transaction : ${group.transactions}">
<h2>Amount: <span th:text="${transactions.amount}"></span></h2><br>
<h2>Note: <span th:text="${transactions.note}"></span></h2><br>
<h2>Wallet name: <span th:text="${transactions.walletName}"></span></h2><br>
<h2>Expense Category: <span th:text="${transactions.expenseCategories}"></span></h2><br>
<h2>IncomeCategory: <span th:text="${transactions.incomeCategories}"></span></h2>
<div>
</div>

Binding checkedboxes as objects to be added to a <List> as a field of parent object, to be passed to database

I am trying to make a Recipe Builder form using Thymeleaf.
The functionality I am trying to implement involves adding a List of selected objects (ingredients) to the bound recipe.
Here is a snippet of the Form from the Template file:
<form action="#" th:action="#{/recipes/userRecipes/new/save}" th:object="${userRecipe}"
method="post">
...
<td>Ingredients:</td>
<td>
<ul>
<li th:each="ingredient : ${allIngredients}">
<input type = "checkbox" th:name="sel_ingredients" th:value="${ingredient.id}" />
<label th:text="${ingredient.name}">Ingredient</label>
</li>
</ul>
</td>
</tr>
...
</table>
</form>
Here is a snippet from my controller:
#RequestMapping("/userRecipes/new")
public String createNewUserRecipe(Model model){
UserRecipe userRecipe = new UserRecipe();
model.addAttribute("userRecipe", userRecipe);
return "new-recipe";
}
#RequestMapping(value = "/userRecipes/new/save", method = RequestMethod.POST)
public String saveRecipe(
#ModelAttribute("userRecipe") UserRecipe userRecipe,
#RequestParam(value = "sel_ingredients", required = false) int[] sel_ingredients,
BindingResult bindingResult, Model model){
for(int i : sel_ingredients){
Ingredient ing = new Ingredient();
ing = ingredientRepo.getOne((long)i);
userRecipe.addIngredient(ing);
}
userRecipeRepo.save(userRecipe);
return "redirect:../";
}
Along with a #ModelAttribute to set Ingredients:
#ModelAttribute
public void populateIngredients(Model model){
List<Ingredient> ingredientList = this.ingredientRepo.findAll();
//Iterator<Ingredient> itr = ingredientList.iterator();
model.addAttribute("allIngredients", ingredientList);
}
Entities:
#Entity
#Table(name = "user_recipes")
public class UserRecipe extends AuditModel{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Size(max = 100)
#Column(unique = true)
private String title;
#NotNull
#Size(max = 250)
private String description;
#ManyToMany
#JoinTable(
name = "used_ingred",
joinColumns = #JoinColumn(name = "user_recipe_id"),
inverseJoinColumns = #JoinColumn(name = "ingredientid")
)
private List<Ingredient> ingredients;
public void addIngredient(Ingredient i){
this.ingredients.add(i);
}
#NotNull
#Lob
private String preparation;
#Entity
#Table(name = "ingredients_master")
public class Ingredient extends AuditModel{
#Column(name = "ingredient")
private String name;
#Column(name="description")
private String description;
#Id
#GeneratedValue
private long id;
// public Ingredient(String n, String d){
// this.setName(n);
// this.setDescription(d);
// }
// public Ingredient(int id, String n, String d){
// this.setId(id);
// this.setName(n);
// this.setDescription(d);
// }
#ManyToMany(mappedBy = "ingredients")
Set<UserRecipe> recipes;
#ManyToMany
Set<Tag> tags;
Originally, I tried to grab the Ingredient objects directly, but couldn't figure out how to do that, so instead opted to grab the ids, and then reference them from the Repository; however,
I get the following error when I click the save button:
There was an unexpected error (type=Internal Server Error, status=500).
No message available
java.lang.NullPointerException
at com.donovanuy.mixmix.entities.UserRecipe.addIngredient(UserRecipe.java:85)
at com.donovanuy.mixmix.controllers.RecipeController.saveRecipe(RecipeController.java:103)
If possible, I'd like to be able to grab the Ingredient object directly, and add it to the List field of my userRecipe.
I am considering making a separate RecipeMakerForm, that I can instead pass into a service. Something like:
public class RecipeMakerForm{
String newTitle;
Long[] ingredientIds;
Long[] tagIds;
and the service would like like:
public class RecipeMaker{
void makeRecipe(RecipeMakerForm form){
Recipe r = new Recipe(form.getNewTitle());
if (ingredientIds != null){
for (Long id : ingredientIds){
r.addIngredient(ingredientRepo.getOne(l));
}
//etc
recipeRepo.save(r);
}
Even still, I suspect my errors have something to do with the way Thymeleaf is passing the model data back to the controller, so my new RecipeMaker & RecipeMakerForm methods wouldn't work. I'm still new to using Thymeleaf and working with Spring.data, so I expect my solution to be somewhere in my Thymeleaf or Controller methods.

How to show nested object value in Thymeleaf form

I have Book and Author entities that I would like to bind to html form (user will insert data for Book with authors and those data will be sent via html form to the mysql database). I do not know how to bind Book.authors.forename and Book.authors.surname values to my thymeleaf form.
Book.java
#Entity
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String isbn;
#ManyToMany(cascade = CascadeType.ALL) // to save author's data when adding book to db
#JsonIgnoreProperties("books")
#JoinTable(name = "author_book", joinColumns = #JoinColumn(name = "book_id"),
inverseJoinColumns = #JoinColumn(name = "author_id"))
private Set<Author> authors = new HashSet<>();
Author.java
#Entity
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String forename;
private String surname;
#ManyToMany(mappedBy = "authors", cascade = CascadeType.ALL) // look -> #ManyToMany in Book
#JsonIgnoreProperties("authors")
private Set<Book> books = new HashSet<>();
BookController.java
(...)
#GetMapping("/book/add")
public ModelAndView addGet() {
ModelAndView m = new ModelAndView();
m.addObject("book", new Book());
m.setViewName("addBook");
return m;
}
#PostMapping("/book/add")
public ModelAndView addBook(Book book, BindingResult br) {
ModelAndView m = new ModelAndView("redirect:/index");
bookService.addBook(book);
m.addObject("book", new Book());
return m;
}
addBook.html
<form th:action="#{/book/add}" th:object="${book}" th:method="post">
<label for="author_name"> Author_Name </label>
<input th:field="*{author}" id="author_name" type="text">
<label for="author_surname"> Author_Surn </label>
<input th:field="*{author}" id="author_surname" type="text">
<label for="title"> Title </label>
<input th:field="*{title}" id="title" type="text">
<label for="isbn"> ISBN </label>
<input th:field="*{isbn}" id="isbn" type="text">
<button type="submit"> Add</button>
</form>
Problem is in the author_name and author_surname label and input.
you can try this code only for view purpose......
<span th:each="authors,iterStat : ${student.authors}">
<span th:text="${authors.forename}"/><th:block th:if="${!iterStat.last}">,</th:block>
</span>
according to your code it's a data entry form .but for many-to-many relation it's not a good decision for insert book table data with author name from same time....
you can save book data first than assign author name .....
for more help click this link

keep open the Hibernate session with spring MVC application

I am getting an invalid property exception when I am trying to bind a collection in my model object in my form object. I was trying some trick and now I think that the problem is hibernate session with spring MVC , so in the controller I edited create function so that before my form saves, I try to get session and get list of typesite's but still get the same error:
org.springframework.beans.InvalidPropertyException: Invalid property ‘siteesTypeSite[idTypeSite]’ of bean class [model.Sites]:
Property referenced in indexed property path ‘siteesTypeSite[idTypeSite]’ is neither an array nor a List nor a Map; returned value was [2]
Is there something wrong with my mapping?
Sites.java mapping
public class Sites implements java.io.Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#JsonBackReference("site-typeSite")
#LazyCollection(LazyCollectionOption.FALSE)
#ManyToOne
#JoinColumn(name = “idTypeSite”)
private TypeSites siteesTypeSite;
Getter/setters
}
TypeSites.java mapping :
public class TypeSites implements java.io.Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="idTypeSite")
private int idTypeSite;
private String typeSite;
#LazyCollection(LazyCollectionOption.FALSE)
#JsonManagedReference("site-typeSite")
#OneToMany(mappedBy = “siteesTypeSite”,fetch = FetchType.LAZY)
// private Set<Sites> sitees= new HashSet<Sites>(0);
private List<Sites> sitees = new AutoPopulatingList<Sites>(Sites.class);
Getter/setters
}
controller class :
#RequestMapping(method = RequestMethod.POST, produces = "application/json")
public ResponseEntity<?> create(
#ModelAttribute("site") #Valid Sites site,
#ModelAttribute("typeSites") TypeSites typeSites,
#RequestParam(required = false) String searchFor,
#RequestParam(required = false,
defaultValue = DEFAULT_PAGE_DISPLAYED_TO_USER) int page,
Locale locale) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession();
TypeSites existingTypeSites = (TypeSites) ((Session) session).get(TypeSites.class, site.getSiteesTypeSite().getIdTypeSite());
site.setSiteesTypeSite(existingTypeSites);
siteService.save(site);
}
Angularjs code :
$scope.createObject = function (newObjectForm) {
if (!newObjectForm.$valid) {
$scope.displayValidationError = true;
return;
}
$scope.lastAction = ‘create';
var url = $scope.url;
var config = {headers: {‘Content-Type': ‘application/x-www-form-urlencoded; charset=UTF-8′}};
$scope.addSearchParametersIfNeeded(config, false);
$scope.startDialogAjaxRequest();
$scope.sites.siteesTypeSite =JSON.parse($scope.sites.siteesTypeSite);
$http.post(url, $.param($scope.sites), config)
.success(function (data) {
$scope.finishAjaxCallOnSuccess(data, “#addObjectsModal”, false);
})
.error(function(data, status, headers, config) {
$scope.handleErrorInDialogs(status);
});
};
JSP : Here I a call to another angular controller to get a list of type site on the select
<div ng-controller="siteTypesiteController">
<select required
ng-model="sites.siteesTypeSite"
name="siteesTypeSite"
ng-options="typesites as typesites.typeSite for typesites in page.source "
>
<option value="">-- Select Type site --</option>
</select><br>
and I have this before submit in the browser :
Object {codeOracle: "test", codeGSM: "test", area: "test", siteesTypeSite: Object}
area: "test"
codeGSM: "test"
codeOracle: "test"
siteesTypeSite: Object
idTypeSite: 2
sitees: Array[0]
typeSite: "Public"
__proto__: Object
__proto__: Object
The problem is : I can't submit the nested object , if I change value of select to typesites.typeSite instead of typesites like this
<div ng-controller="siteTypesiteController">
<select required
ng-model="sites.siteesTypeSite"
name="siteesTypeSite"
ng-options="typesites.typeSite as typesites.typeSite for typesites in page.source "
>
<option value="">-- Select Type site --</option>
</select><br>
The submit work fine, but I have two inserts : an insert in the typesite table, and an insert in the site table with the new foreign key.

Spring MVC + Hibernate: How to handle form with relation

I have problem with adding/updating records with relations. Could please some advice how it should work?
I have two entities: Question and Category:
public class Question {
#Id
#GeneratedValue
private Long questionId;
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "category")
private Category category;
and
#Entity
#Table(name = "category")
public class Category {
#Id
#GeneratedValue
private Long categoryId;
private String name;
I have some list of categories and I would like to add new Question with selected Category. So in my QuestionController I have add method:
#RequestMapping(value = "/add", method = RequestMethod.GET)
public ModelAndView add() {
ModelAndView mav = new ModelAndView("question/add");
mav.addObject("question", new Question());
mav.addObject("categoryList", categoryService.getAll());
return mav;
}
and form:
<form:form modelAttribute="question" method="POST" >
Name: <form:input path="name" value="${ques.name}" />
Category: <form:select path="category" items="${categoryList}" />
<input type="submit" value="Add" />
</form:form>
Everything looks good for now (I can fill question name and select category). But I don't know how add POST method should work
#RequestMapping(value = "/add", method = RequestMethod.POST)
public String added(#ModelAttribute Question question, BindingResult bindingResult) {
}
When I try to use above method I have error: Failed to convert property value of type 'java.lang.String' to required type model.Category
I've tried to look for similar problem but I coudln't find anything.. So if someone can help/advice or show link to similar issue I would be grateful!
Cheers!
You need to provide code for Spring that tells it how to convert the string value from the web page back into a Category object. This is done by either:
Adding a PropertyEditor to the DataBinder.
Creating a Converter.
It is a bad practice using hibernate objects to map the form items. There are two solutions
Add another property private transient String categoryString; to the 'Question' class. and map the UI category to this <form:select path="categoryString" items="${categoryList}" />
That way you can avoid the error.
Do not use the hibernate mapping classes for mapping the form items, use POJOs for doing this. and later somewhere in your application map this simple pojo elements onto the hibernate entity.
try changing this line:
<form:select path="category" items="${categoryList}" />
to:
<form:select path="category.categoryId" items="${categoryList}" itemLabel="name" itemValue="categoryId"/>
Then in the added method (post method), retrieve the Category object back from hibernate and set back on question object before saving:
Category selectedCategory = yourHibernateService.getCategoryById(question.getCategory().getCategoryId());
question.setCategory(selectedCategory);

Categories