Spring MVC + Hibernate: How to handle form with relation - java

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);

Related

Thymeleaf Option Without Selection

From the controller, I have returned an object containing a list of objects. I want to display these list of objects in the dropdown with no pre-selection(or default value i.e. "Select Dish"), but dropdown is shown with pre-selected last value in the list.
Controller:
#GetMapping(path = "/createOrder")
public ModelAndView displayOrder(OrderFormDetails order) {
ModelAndView mav = new ModelAndView();
mav.addObject("order", orderService.displayOrder());
mav.setViewName("createOrder");
return mav;
}
Model:
public class OrderFormDetails {
#NotEmpty(message = "*Please provide your name")
private String name;
#NotEmpty(message = "*Please provide your address")
private String address;
private List < Dish > dishes;
View:
<select class="form-control" th:field="*{dishes}" id="dropOperator">
<option value="" selected="selected">Sélect dish</option>
<option th:each="dish, itemStat : *{dishes}" th:value="*{dishes[__${itemStat.index}__].id}" th:text="*{dishes[__${itemStat.index}__].title}">
</option>
</select>
I have tried multiple tricks, but none of them worked. Thanks...
Usually you don't want to mix possible options and selected option together within one field (as you apparently did). All you need to do is to decouple those things. Possible steps may be helpful:
Since OrderFormDetails acts like your form backing bean, it should contain a placeholder for selected value (Dish.id) instead of List with possible dishes. Change your OrderFormDetails to the following:
public class OrderFormDetails {
#NotEmpty(message = "*Please provide your name")
private String name;
#NotEmpty(message = "*Please provide your address")
private String address;
private T selectedDish;
// getters, setters, rest of the class omitted
...where T is the type assigned to Dish.id field.
Possible (selectable) dishes should be provided separately as a model attribute. Add following line to your displayOrder method:
mav.addObject("dishes", getDishes());
...where getDishes() returns List<Dish> containing all dishes being an option for an user.
Adjust your view to work with updated approach:
<select class="form-control" th:field="*{selectedDish}" id="dropOperator">
<option value="" selected="selected">Sélect dish</option>
<option th:each="dish : ${dishes}" th:value="${dish.id}" th:text="${dish.title}">
</option>
</select>
That's all. Such an approach is also shown in documentation - th:field on <select> level refers to form backing bean's field, whereas <option> elements are created out of separate collection provided as model attribute.

Controller for #OneToMany relationship using Spring Boot

I'm trying to figure out with Spring Boot and databases.
So I have 2 entities with #OneToMany relationship:
#Entity
public class Team {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int teamId;
#Column
private String teamTitle;
#Column
private String teamCity;
#ManyToOne
#JoinColumn(name = "conferenceId", nullable = false)
private Conference teamConference;
public Team() { super(); }
//some getters and setters
}
And the second one:
#Entity
public class Conference {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int conferenceId;
private String conferenceTitle;
#OneToMany(mappedBy = "teamId")
private List<Team> conferenceTeams;
public Conference() {
super();
}
//some getters and setters
}
Jsp page:
<body>
<form:form method="post" modelAttribute="team">
<div>
<form:label path="teamTitle">Title</form:label>
<form:input path="teamTitle" type="text"/>
<form:label path="teamCity">City</form:label>
<form:input path="teamCity" type="text"/>
//DAHELL IS HERE
<div class="form-group">
<label for="conferenceList">Select conference:</label>
<select class="form-control" id="conferenceList">
<c:forEach items="${conference}" var="conf">
<option>${conf.conferenceTitle}</option>
</c:forEach>
</select>
</div>
<button type="submit" class="btn btn-success">Add</button>
</div>
</form:form>
// jquery etc
</body>
And controller class:
#Controller
public class TeamsController {
#Autowired
private TeamDAO teamDAO;
#Autowired
private ConferenceDAO conferenceDAO;
#RequestMapping(value = "/schedule", method = RequestMethod.GET)
public String showSchedule(ModelMap model) {
model.put("conferences", conferenceDAO.findAll());
model.put("teams", teamDAO.findAll());
return "schedule";
}
#RequestMapping(value = "/new-team", method = RequestMethod.GET)
public String addNewTeam(ModelMap model) {
model.addAttribute("conference", conferenceDAO.findAll());
model.addAttribute("team", new Team());
return "new-team";
}
#RequestMapping(value = "/new-team", method = RequestMethod.POST)
public String addTeam(ModelMap model, Team newTeam) {
teamDAO.save(newTeam);
return "redirect:/schedule";
}
}
ConferenceDAO and TeamDAO are just interfaces extended from JpaRepository.
So what I'm trying to understand is how to add new Team. I insert title and city through jsp page and also I should choose which conference this team belongs. But when I press add button I got
There was an unexpected error (type=Internal Server Error, status=500).
No message available
What am I doing wrong? I believe that something with selecting part in jsp page. And I'm 100% sure I'm missing something in my Controller class. Somehow I should save new team to my DB and the Conference column also should show that it contains this new team.
I'd really appreciate guys if you show me the way to dig up.
So yes, comments really helped. So for those who read it - sleep on the problem and read logs before ask dumb questions (as I did :D )
The problem was within jsp page and selection form. I was getting Null for conferenceId and I would know it if I'd read logs.
So comments about checking whole stack trace and changing jsp selection part - worked for me.
Fixed jsp selection is:
<div class="form-group">
<label for="conferenceList">Select conference:</label>
<form:select path="teamConference" id="conferenceList">
<form:options items="${conference}" itemLabel="conferenceTitle"/>
</form:select>
</div>
Thx guys!

Detached entity passed to persist when save the child data

I'm getting this error when submitting the form:
org.hibernate.PersistentObjectException: detached entity passed to persist: com.project.pmet.model.Account; nested exception is javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.project.pmet.model.Account
Here are my entities:
Account:
#Entity
#DynamicInsert
#DynamicUpdate
public class Account {
#Id
#GeneratedValue
private Integer id;
#Column(nullable = false)
private String login;
#Column(nullable = false)
private String password;
#Column(nullable = false)
private String email;
#ManyToOne
#JoinColumn(name = "team_id")
private Team team;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
private List<Team> ownedTeams;
...
Team:
#Entity
#DynamicInsert
#DynamicUpdate
public class Team {
#Id
#GeneratedValue
private Integer id;
#Column(nullable = false)
private String name;
#ManyToOne
#JoinColumn(name = "owner_id", nullable = false)
private Account owner;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "team")
private List<Account> members;
...
Here's a part of the Controller:
#ModelAttribute("team")
public Team createTeamObject() {
return new Team();
}
#RequestMapping(value = "/teams/create-team", method = RequestMethod.GET)
public String getCreateTeam(#ModelAttribute("team") Team team, Principal principal) {
logger.info("Welcome to the create team page!");
Account owner = accountService.findOneByLogin(principal.getName());
team.setOwner(owner);
team.setMembers(new AutoPopulatingList<Account>(Account.class));
return "teams";
}
#RequestMapping(value = "/teams/create-team", method = RequestMethod.POST)
public String postCreateTeam(#ModelAttribute("team") Team team) {
logger.info("Team created!");
teamService.save(team);
return "redirect:/teams.html";
}
And the form:
<form:form commandName="team" id="teamForm">
<div class="form-group">
<label>Name</label>
<form:input path="name" cssClass="form-control" />
</div>
<div class="form-group" id="row-template">
<label>Members</label>
<form:select path="members[0].id" cssClass="form-control" data-live-search="true" >
<form:options items="${accounts}" itemValue="id" />
</form:select>
...
</div>
<form:hidden path="owner.id" />
</form:form>
What am I doing wrong?
teamService.save(team);
Save method accepts only transient objects. What is the transient object you can find here
Transient - an object is transient if it has just been instantiated using the new operator, and it is not associated with a Hibernate Session. It has no persistent representation in the database and no identifier value has been assigned. Transient instances will be destroyed by the garbage collector if the application does not hold a reference anymore. Use the Hibernate Session to make an object persistent (and let Hibernate take care of the SQL statements that need to be executed for this transition).
You are getting the Team object and you are trying to persist it to the DB but that object has Account object in it and that Account object is detached (means that instance of that object has saved into the DB but that object is not in the session). Hibernate is trying to save it because of you have specified:
#OneToMany(cascade = CascadeType.ALL, ....
So, there are few ways how you can fix it:
1) do not use CascadeType.ALL configuration. Account object can be used for number of Teams (at least domain structure allows it) and update operation might update Account for ALL Teams -- it means that this operation should not be initiated with Team update.
I would remove cascade parameter from there (default value is no cascade operations), of if you really need use MERGE/DELETE configuration. But if you really need to persist it then see option #2
2) use 'saveOrUpdate()' method instead of 'save()'. 'saveOrUpdate()' method accepts transient and detached objects.
But the problem with this approach is in design: do you really need to insert/update account when you are saving Team object? I would split it in two operations and prevent updating Account from the Team.
Hope this helps.
The error occurs because the id is set. Hibernate distinguishes between transient and detached objects and persist works only with transient objects.
isteamService.save(team);
in this operation can not be loaded id because is #GeneratedValue
Please, change #OneToMany(cascade = CascadeType.ALL,..) to #OneToMany(cascade = CascadeType.REMOVE,...) or another except CascadeType.PERSIST and the problem has been solved
Since your id is auto generated value, don't send it from client side. I had a same issue. Make sure that you does't provide a value for auto generated attribute.
This error happened for me when I tried to save a child entity and then pass the newly saved entity as parameter to a new parent object.
For instance:
ChildA a = childAService.save(childAObject);
Parent parent = new Parent();
parent.setChildA(a) // <=== Culprit
parentService.save(parent);
Instead, do:
ChildA a = new ChildA();
parent.setChildA(a)
parentService.save(parent)
Hibernate handles the persisting of a for you, you don't have to do it yourself.
Be aware of Lombok .toBuilder() method - it is creating a new instance of the object, which can be pretty misleading, when you are trying to update the part of a child object.
Example:
public class User {
#OneToOne(...)
Field x;
}
#Builder(toBuilder = true)
public class Field {
String a;
String b;
}
#Transactional
public class UserService {
public updateUserField(User user) {
...
user.setX(user.getX().toBuilder().a("New value").build());
}
}
This will throw the PersistentObjectException without explicitly calling the userRepo.save method.
You need to do:
var x = user.getX();
x.setA("New Value");

Spring form binding drop down object

Facing issue in spring form binding.Say i have two models Category and Product.
#Entity
#Table(name="PRODUCT")
public class Product
{
#Id
#GeneratedValue
private long productID;
#ManyToOne
#JoinColumn(name="categoryID")
private Category category;
//Getters and setters
}
#Entity
#Table(name = "CATEGORY")
public class Category {
#Id
#GeneratedValue
private long categoryID;
private String categoryName;
}
In controller to render add product page
#RequestMapping(value = "/productpage", method=RequestMethod.GET)
private ModelAndView getAddProductPage(){
ModelAndView modelAndView = new ModelAndView("add-product","product",new Product());
Map<Category,String> categoriesMap = new HashMap<Category, String>();
List<Category> categories = categoryService.getAllCategories();
if(categories != null && !categories.isEmpty())
{
for(Category category : categories)
{
categoriesMap.put(category, category.getCategoryName());
}
}
modelAndView.addObject("categoryList",categories);
return modelAndView;
}
I am able to populate the drop down values of categories in JSP page using below code :
<form:select path="category" >
<form:options items="${categoryList}"/>
</form:select>
While submitting the form i'm facing error 400 The request sent by the client was syntactically incorrect.Failed to convert property value of type 'java.lang.String' to required type 'com.example.model.Category' for property 'category'.
If i view page source for each option category is assigned correctly. But not understanding why spring throwing that err.. Need help. Thanks in advance!
This worked for me!
<form:select path="category.categoryID" >
<form:options items="${categoryList}" itemValue="categoryID" />
</form:select>
You should make the following change in your Controller's action:
for(Category category : categories)
{
categoriesMap.put(category.geCategorytId(), category.getCategoryName());
}
and in your view change:
<form:select path="category.categoryID" >
The select drop-down will have the category name as the display text and category ID as the value.
<form:select path="category">
<c:forEach items="${categoryList}" var="category">
<form:option value="${category}">${category.categoryName}</form:option>
</c:forEach>
</form:select>
or
<form:select class="form-control" path="site">
<form:option value="-1">Select...</form:option>
<form:options items="${categoryList}" itemValue="categoryID" itemLabel="categoryName"/>
</form:select>

Creating drop down list using Spring, Hibernate, JSP

Applications: Hibernate, Spring 3.0 MVC, JSP (used Spring forms)
Requirement: To select a table data from the database using Hibernate and display that as a drop-down list in JSP page using Spring MVC.
Code:
Hibernate / Dao code is
Cuisine class
#Entity
#Table(name = "cuisine")
public class Cuisine {
#Id
#Column(name = "id")
private int id;
#Column(name = "name")
private String name;
.. getters and setters
RecipeDaoImpl class
public List<Cuisine> getCuisine() {
String hql = "SELECT id, name FROM Cuisine";
return getSession().createQuery(hql).list();
}
Spring MVC
#Controller
public class RecipeController {
...
#RequestMapping("/add")
public String newRecipe(Map<String, Object> map) {
/* Get cuisine list and new object for cuisine */
List<Cuisine> cuisines = recipeServices.getCuisine();
System.out.println(cuisines);
map.put("cuisineList", cuisines);
map.put("cuisine", new Cuisine());
return "recipes/new";
}
JSP page:
<%# taglib prefix="sf" uri="http://www.springframework.org/tags/form"%>
<tr>
<th><sf:label path="cuisine">Cuisine</sf:label></th>
</tr>
<tr>
<td><sf:select path="${cuisineList}">
<sf:options items="${cuisine}"></sf:options>
</sf:select></td>
</tr>
On doing it, I am getting following error:
org.springframework.beans.NotReadablePropertyException: Invalid property '[Cuisine [id=1, name=Continental][id=2, name=Italian]' of bean class [com.recipe.tables.Recipe]: Bean property '[Cuisine [id=1, name=Continental][id=2, name=Italian]' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:729)
org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:721)
org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:99)
org.springframework.validation.AbstractBindingResult.getFieldValue(AbstractBindingResult.java:219)
org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:120)
Can someone please suggest how to fix this? I have checked couple of articles and tried to replicate them, but no luck.
I think jsp should be
<td><sf:select path="${cuisine}">
<sf:options items="${cuisineList}" id="id" itemValue="name"></sf:options>
</sf:select></td>
</tr>

Categories