I have problem with validation in thymeleaf. My case is to save Employee with Position and Role-s. Those two "fields" cause LazyInitializationException when validation has errors. If validation passed Employee will be save to DB and everything is ok. Please give me some advice, what I am doing wrong or how can I fix it.
Please look for my code below:
EmployeeController:
#Controller
public class EmployeeController extends BaseCrudController {
// (........)
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.GET)
public String createEmployee(Model model) {
prepareEmployeeForm(model);
return "crud/employee/create";
}
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, #ModelAttribute("employeeForm") #Valid EmployeeForm employeeForm, BindingResult result) {
if (!result.hasErrors()) {
User user = employeeFormService.getUserFromEmployeeForm(employeeForm);
try {
userService.merge(user);
model.addAttribute("success", true);
prepareEmployeeForm(model);
} catch (Exception e) {
model.addAttribute("error", true);
}
} else {
initCollections(employeeForm, model);
}
return "crud/employee/create";
}
private void initCollections(EmployeeForm employeeForm, Model model)
{
employeeForm.setAllAvailableRoles(roleRepository.findAll());
employeeForm.setAllAvailablePositions(positionRepository.findByEnabledTrueOrderByNameAsc());
model.addAttribute("employeeForm", employeeForm);
}
private void prepareEmployeeForm(Model model) {
EmployeeForm employee = new EmployeeForm();
employee.setAllAvailablePositions(positionRepository.findByEnabledTrueOrderByNameAsc());
employee.setAllAvailableRoles(roleRepository.findAll());
model.addAttribute("employeeForm", employee);
}
}
EmployeeForm:
public class EmployeeForm extends BaseForm {
#Length(min = 2, max = 45)
private String firstName = "";
// (........)
private Position position;
private Collection<Role> roles;
private Collection<Position> allAvailablePositions;
private Collection<Role> allAvailableRoles;
public EmployeeForm() {
}
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
public Collection<Role> getRoles() {
return roles;
}
public void setRoles(Collection<Role> roles) {
this.roles = roles;
}
public Collection<Position> getAllAvailablePositions() {
return allAvailablePositions;
}
public void setAllAvailablePositions(Collection<Position> allAvailablePositions) {
this.allAvailablePositions = allAvailablePositions;
}
public Collection<Role> getAllAvailableRoles() {
return allAvailableRoles;
}
public void setAllAvailableRoles(Collection<Role> allAvailableRoles) {
this.allAvailableRoles = allAvailableRoles;
}
}
employeeForm.html
<form action="#" th:action="#{/panel/employee/create}" th:object="${employeeForm}" method="post">
<!--(......)-->
<div class="row">
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<!--(Line 57 cause LazyInitializationException)--><select th:field="*{position}" class="form-control">
<option th:each="positionQ : *{allAvailablePositions}"
th:value="${{positionQ}}"
th:text="${positionQ.name}">Position name
</option>
</select>
</div>
<div class="col-md-6">
<label th:text="#{permissions}">Permissions</label>
<th:block th:each="role : *{allAvailableRoles}">
<p>
<input type="checkbox" th:id="${{role}}" th:value="${{role}}" th:field="*{roles}"/>
<label th:for="${{role}}"
th:text="#{${role.name}}">Role name</label>
</p>
</th:block>
</div>
</div>
</form>
Trace:
HTTP Status 500 - Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring4.processor.attr.SpringSelectFieldAttrProcessor' (crud/employee/employeeForm:57)
root cause:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring4.processor.attr.SpringSelectFieldAttrProcessor' (crud/employee/employeeForm:57)
root cause
org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'org.thymeleaf.spring4.processor.attr.SpringSelectFieldAttrProcessor' (crud/employee/employeeForm:57)
root cause
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
I will be really glad for any help.
The problem is that your hibernate session is closed. The pattern open-session-in-view solves this issue. You can use spring-boot where it is the default or look at the configuration in fuwesta-sampe.
The cleaner approach is to ensure that the data are complete loaded before you close the session. This means a service layer should navigate to each entity or use eager fetching.
Related
I'm trying to add data to my database and reload the same page using spring boot and thymeleaf but when I save data I face this error
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'org.closure.gcp.entities.QuestionEntity'; nested exception
is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value 'adsf'; nested exception is java.lang.NumberFormatException: For input string: "adsf"
controller code :
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#RequestMapping(path = "/")
public String index(#ModelAttribute("question") QuestionEntity question, Model model)
{
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
#RequestMapping(value="/add", method=RequestMethod.POST)
public String addQuestion(Model model,#ModelAttribute("question") QuestionEntity question) {
questionRepo.save((QuestionEntity)model.getAttribute("question"));
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
}
thymeleaf page :
<html>
<header>
<title>Questions</title>
</header>
<body>
<h2>hello questions</h2>
<hr>
<tr th:each="q: ${questions}">
<td th:text="${q.question}"></td>
<br>
<td th:text="${q.question_type}"></td>
<hr>
</tr>
<!-- <form th:action="#{/add}" th:object="${question}" method="post"> -->
<form action="./add" th:object="${question}" method="POST">
<input type="text" th:field="*{question}" />
<br >
<input type="text" th:field="*{question_type}" />
<br >
<input type="submit" value="save" >
</form>
</body>
</html>
#Entity
#Table(name="question")
public class QuestionEntity {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
private Integer id;
#Column(nullable=false)
private String question;
#Column(nullable=false)
private String question_type;
#ManyToOne(optional = true)
private InterestEntity interest;
#ManyToOne(optional = true)
private LevelEntity level;
#Column(nullable = true)
private String sup_file;
#Column(nullable = false)
private int pionts;
#ManyToMany
private List<ContestEntity> contest;
#OneToMany(mappedBy ="question")
private List<AnswerEntity> answers;
// getters and setters
}
notice when I try to open another page in "/add" it works
I found this to solve
I just made a model class and use it instead of entity
and I used just one method to handle index and add requests
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#RequestMapping(path = {"/",""},method = {RequestMethod.POST,RequestMethod.GET})
public String index(#ModelAttribute("question") QuestionModel question, Model model,HttpServletRequest request)
{
if(request.getMethod().equals("POST"))
{
questionRepo.save(new QuestionEntity().question(question.getQuestion()).question_type(question.getQuestion_type()));
}
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
}
It is better to have 2 separate methods, one for GET and one for POST and to use redirect after the POST (see https://en.wikipedia.org/wiki/Post/Redirect/Get). This is how I would code this based on your separate QuestionModel class:
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#GetMapping
public String index(Model model)
{
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
model.addAttribute("question", new QuestionModel());
return "Questions";
}
#PostMapping("/add")
public String addQuestion(#Valid #ModelAttribute("question") QuestionModel question, BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
return "Questions";
}
questionRepo.save(new QuestionEntity().question(question.getQuestion()).question_type(question.getQuestion_type()));
return "redirect:/Questions";
}
}
Main points:
Use separate methods for GET and POST
Add the #Valid annotation to the #ModelAttribute in the POST method so any validation annotations on QuestionModel are checked (Because you probably want to make sure the question has at least some text in it for example).
Use BindingResult as parameter to check if there are validation errors.
Use "redirect:" to force a new GET after the POST to help avoid double submissions if a user would refresh the browser.
Error Details
The page works okay, but when I put incorrect period of time into two inputs startStatusDate and endStatusDate. Error occurred while validation check an error in class DriverHistoryValidator.
I see in debugger that exception generated after assignment
errors.rejectValue("startStatusDate",
"co.driverHistoryStatusPeriod.notpermitted")
in method
validate(Object o, Errors errors)
See below please
What am I doing wrong?
2018-05-03 11:03:54.364 ERROR 6234 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.beans.NotReadablePropertyException: Invalid property 'startStatusDate' of bean class [kg.nurtelecom.dictionaries.entity.carordering.Driver]: Bean property 'startStatusDate' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?] with root cause
org.springframework.beans.NotReadablePropertyException: Invalid property 'startStatusDate' of bean class [kg.nurtelecom.dictionaries.entity.carordering.Driver]: Bean property 'startStatusDate' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
Front End
Front end implemented in Thymeleaf:
<form id="statusSheduleForm" class="form-horizontal" th:action="#{/driver/saveStatusSchedule}"
th:method="POST"
th:object="${driverHistory}">
<div class="form-group col-md-7">
<div class="input-group date">
<label class="validation-message" for="statusdatetimepicker1"
th:if="${#fields.hasErrors('startStatusDate')}"
th:errors="*{startStatusDate}"></label>
<input type="text" placeholder="Время начала" th:field="*{startStatusDate}"
id="statusdatetimepicker1"/>
<input type="text" placeholder="Время окончания" th:field="*{endStatusDate}"
id="statusdatetimepicker2"/>
<select id="status-select" required="required" th:field="*{driverStatus}">
<option th:each="item:${statuses}"
th:value="${item.id}"
th:text="${item.name}"></option>
</select>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Сохранить</button>
<a th:href="#{/driver/index}" class="btn btn-default">Закрыть</a>
</div>
</form>
BackEnd:
Controller
#RequestMapping(value = "/saveStatusSchedule", method = RequestMethod.POST)
public ModelAndView saveStatusSchedule(#ModelAttribute DriverHistory driverHistory, Driver driver,
BindingResult bindingResult) {
ModelAndView modelAndView = getModelsViews();
Driver sessionDriver = (Driver) modelAndView.getModel().get("driver");
if (sessionDriver != null) {
sessionDriver.setMenu1Act();
driverHistory.setDriver(sessionDriver);
driverHistoryValidator.validate(driverHistory, bindingResult);
if (bindingResult.hasErrors()) {
return getModelsViews();
}
if (driverHistory.getDriverShift() != null) {
sessionDriver.setMenu2Act();
}
driverHistory.setDriver(sessionDriver);
driverHistoryService.save(driverHistory);
return getModelsViews();
} else {
driver.setMenu0Act();
modelAndView.addObject("failMessage", "Водитель не создан");
modelAndView.addObject("driver", driver);
return modelAndView;
}
}
Validator:
#Component
public class DriverHistoryValidator implements Validator {
#Autowired
DriverHistoryService driverHistoryService;
#Override
public boolean supports(Class<?> aClass) {
return DriverHistory.class.equals(aClass);
}
#Override
public void validate(Object o, Errors errors) {
DriverHistory driverHistory = (DriverHistory) o;
if (driverHistoryService.isExistDriverStatusInPeriodOf(driverHistory)) {
errors.rejectValue("startStatusDate", "co.driverHistoryStatusPeriod.notpermitted");
}
}
}
Entity:
#Entity
#Table(name = "CO_DRIVER_HISTORY")
public class DriverHistory extends BaseEntity {
#DateTimeFormat(pattern = "dd.MM.yyyy HH:mm")
#Column(name = "startStatusDate")
private Date startStatusDate;
#DateTimeFormat(pattern = "dd.MM.yyyy HH:mm")
#Column(name = "endStatusDate")
private Date endStatusDate;
#DateTimeFormat(pattern = "dd.MM.yyyy HH:mm")
#Column(name = "startShiftDate")
private Date startShiftDate;
#DateTimeFormat(pattern = "dd.MM.yyyy HH:mm")
#Column(name = "endShiftDate")
private Date endShiftDate;
#ManyToOne
#JoinColumn(name = "DriverId")
private Driver driver;
#ManyToOne
#JoinColumn(name = "DriverStatusId")
private DriverStatus driverStatus;
#ManyToOne
#JoinColumn(name = "DriverShiftId")
private DriverShift driverShift;
public DriverHistory() {
}
public Date getStartStatusDate() {
return startStatusDate;
}
public void setStartStatusDate(Date startStatusDate) {
this.startStatusDate = startStatusDate;
}
public Date getEndStatusDate() {
return endStatusDate;
}
public void setEndStatusDate(Date endStatusDate) {
this.endStatusDate = endStatusDate;
}
public Date getStartShiftDate() {
return startShiftDate;
}
public void setStartShiftDate(Date startShiftDate) {
this.startShiftDate = startShiftDate;
}
public Date getEndShiftDate() {
return endShiftDate;
}
public void setEndShiftDate(Date endShiftDate) {
this.endShiftDate = endShiftDate;
}
public Driver getDriver() {
return driver;
}
public void setDriver(Driver driver) {
this.driver = driver;
}
public DriverStatus getDriverStatus() {
return driverStatus;
}
public void setDriverStatus(DriverStatus driverStatus) {
this.driverStatus = driverStatus;
}
public DriverShift getDriverShift() {
return driverShift;
}
public void setDriverShift(DriverShift driverShift) {
this.driverShift = driverShift;
}
}
I have figured out the root cause of issue. Let me answer to self asked question.
In my case the org.springframework.beans.NotReadablePropertyException generated since the property 'startStatusDate' is really not exists in class [kg.nurtelecom.dictionaries.entity.carordering.Driver]. Let me explain why it happened. Point is that the parameters of Controller's method mentioned below have incorrect sequence.
#RequestMapping(value = "/saveStatusSchedule", method = RequestMethod.POST)
public ModelAndView saveStatusSchedule(#ModelAttribute
DriverHistory driverHistory,
Driver driver,
BindingResult bindingResult)
BindingResult object declared in parameter obtains previous declared parameter as target object. See debug in screen shot. In my validator class DriverHistoryValidator I set error value to the field of class that is not target object. The issue has fixed after I simply changed the order of parameters in the method above like:
#RequestMapping(value = "/saveStatusSchedule", method = RequestMethod.POST)
public ModelAndView saveStatusSchedule(DriverHistory driverHistory,
BindingResult bindingResult,
Driver driver)
After the reorder of parameter, class DriverHistory is obtained as target object in BindinResult object and validation works properly.
I have a project based in Spring Web model-view-controller (MVC) framework. The version of the Spring Web model-view-controller (MVC) framework is 3.2.8.
This class
public class DeviceForm {
Device device;
List<String> selectedItems = Collections.emptyList();
public DeviceForm() {
super();
}
public Device getDevice() {
return device;
}
public void setDevice(Device device) {
this.device = device;
}
public List<String> getSelectedItems() {
return selectedItems;
}
public void setSelectedItems(List<String> selectedItems) {
this.selectedItems = selectedItems;
}
}
and this
public class Device implements java.io.Serializable {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "CRITERIA")
private BaseCriteria criteria;
public BaseCriteria getCriteria() {
return criteria;
}
public void setCriteria(BaseCriteria criteria) {
this.criteria = criteria;
}
}
and this
#Entity
#Table(name = "CRITERIA")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING)
#SequenceGenerator(name = "seqCriteria", sequenceName = "SEQ_CRITERIA", allocationSize = 1)
public abstract class BaseCriteria {
public BaseCriteria() {
super();
}
private Long id;
private String code;
private Date adoptionDate;
private Date expirationDate;
#Transient
public abstract String getGroupKey();
#Transient
public abstract Long getGroupId();
#Transient
public abstract String getRefColumnName();
#Id
#Column(name = "ID", unique = true, nullable = true)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqCriteria")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "CODE")
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
#Column(name = "ADOPTION_DATE")
#Temporal(TemporalType.TIMESTAMP)
public Date getAdoptionDate() {
return adoptionDate;
}
public void setAdoptionDate(Date adoptionDate) {
this.adoptionDate = adoptionDate;
}
#Column(name = "EXPIRATION_DATE")
#Temporal(TemporalType.TIMESTAMP)
public Date getExpirationDate() {
return expirationDate;
}
#Transient
public boolean isExpired() {
return getExpirationDate().before(new Date());
}
public void setExpirationDate(Date expirationDate) {
this.expirationDate = expirationDate;
}
#Override
public String toString() {
return "BaseCriteria [id=" + id + ", code=" + code + ", adoptionDate="
+ adoptionDate + ", expirationDate=" + expirationDate + "]";
}
}
and the JSP
<form:form commandName="deviceForm"
name="deviceForm"
id="deviceFormId"
method="post"
action="${contextPath}/newdesign/manage/device/${deviceForm.device.id}"
htmlEscape="yes">
<div class="col-sm-6 text-right">
<button class="btn btn-primary" type="submit">Save device</button>
</div>
</div>
<c:forEach items="${deviceForm.device.productGroup.criteria}" var="criteria">
<div class="row">
<div class="col-md-3">
<form:radiobutton path="device.criteria.id" value="${criteria.id}"/>
<label for="basic-url">Criteria:</label>
<input value="${criteria.code}" disabled="disabled" class="form-control"/>
</div>
<div class="col-md-3">
<label for="basic-url">Adoption date:</label>
<input value="<fmt:formatDate type="date" value="${criteria.adoptionDate}" />" disabled="disabled" class="form-control"/>
</div>
<div class="col-md-3">
<label for="basic-url">Expiration Date:</label>
<input value="<fmt:formatDate type="date" value="${criteria.expirationDate}" />" disabled="disabled" class="form-control"/>
</div>
</div>
</c:forEach>
</form:form>
The controller:
/**
* #throws Exception
*
*/
#RequestMapping(value = { "/newdesign/manage/device/{appId}",
"/newdesign/manage/device/{appId}/"}, method = {RequestMethod.GET})
public String viewDevicesWithStatus(
#ModelAttribute("deviceForm") DeviceForm deviceForm,
#PathVariable Long appId,
HttpServletRequest request,
Model model ) throws Exception {
Device device = manageLicenseService.getDeviceById(appId, true);
if (device.getCriteria()==null) {
device.setCriteria(device.getProductGroup().getCriteria().get(0));
}
deviceForm.setDevice(device);
fillModel (model, request, device);
return "cbViewDeviceInfo";
}
/**
* #throws Exception
*
*/
#RequestMapping(value = { "/newdesign/manage/device/{appId}",
"/newdesign/manage/device/{appId}/"}, method = {RequestMethod.POST})
public String saveDevicesWithStatus(
#ModelAttribute("deviceForm") DeviceForm deviceForm,
#PathVariable Long appId,
HttpServletRequest request,
Model model ) throws Exception {
Device device = manageLicenseService.getDeviceById(deviceForm.getDevice().getId());
if (device.getCriteria()==null) {
device.setCriteria(device.getProductGroup().getCriteria().get(0));
}
//TODO: audit
device.updateDevice(deviceForm.getDevice());
manageLicenseService.saveDevice(device);
if (device.getCriteria()==null) {
device.setCriteria(device.getProductGroup().getCriteria().get(0));
}
deviceForm.setDevice(device);
fillModel (model, request, device);
return "cbViewDeviceInfo";
}
But I got following error when I submitted the form, on GET method I got same page without error
org.springframework.beans.NullValueInNestedPathException: Invalid property 'device.criteria' of bean class [com.tdk.iot.controller.newdesign.manage.DeviceForm]: Could not instantiate property type [com.tdk.iot.domain.criteria.BaseCriteria] to auto-grow nested property path: java.lang.InstantiationException
You get the error because in your form you have this:
<form:radiobutton path="device.criteria.id" value="${criteria.id}"/>
and in your POST handler you have this:
public String saveDevicesWithStatus(#ModelAttribute("deviceForm") DeviceForm deviceForm){
}
which means that the MVC framework will try to automatically set the property
deviceForm.device.criteria.id.
Now, because there is no existing DeviceForm in any scope then it will create a new one and of course device.getCriteria() returns null,
hence the exception.
You may think that the DeviceForm you created and populated in the GET handler will be used however Spring MVC is stateless so you
would need to store it in Session scope between requests for it to be re-used or otherwise rework your logic.
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-modelattrib-method-args
.... Given the above example where can theinstance come from? There
are several options.....[in the absence of any other option] It may
be instantiated using its default constructor
A better approach however is to change your form to be as below:
<form:radiobutton path="device.criteria" value="${criteria.id}"/>
and register a converter that would convert the submitted parameter and bind the corresponding entity instance.
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html#core-convert
#Component
public class StringToCriteriaConverter implements Converter<String, BaseCriteria> {
#Autowired
private CriteriaService service;
//source is the ID passed by the page
public BaseCriteria convert(String source) {
// lookup and return item with corresponding ID from the database
}
}
I have a problem with binding collections using spring and thymeleaf. Every time I send form, my object collections are set to null (User.postions), my example below:
My Controller:
#RequestMapping(value = urlFragment + "/add", method = RequestMethod.GET)
public String addPosition(Model model) {
HashSet<Position> positions = new HashSet<Position>(positionRepository.findByEnabledTrueOrderByNameAsc());
User employee = new User();
for (Position position : positions) {
employee.addPosition(position);
}
model.addAttribute("employee", employee);
return "crud/employee/add";
}
#RequestMapping(value = urlFragment + "/add", method = RequestMethod.POST)
public String processNewEmployee(Model model, #Valid #ModelAttribute("employee") User employee, BindingResult result) {
String templatePath = "crud/employee/add";
if (!result.hasErrors()) {
userRepository.save(employee);
model.addAttribute("success", true);
}
return templatePath;
}
And my employee form:
<form action="#" th:action="#{/panel/employee/add}" th:object="${employee}" method="post">
<div class="row">
<div class="col-md-6">
<label th:text="#{first_name}">First name</label>
<input class="form-control" type="text" th:field="*{userProfile.firstName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{last_name}">Last name</label>
<input class="form-control" type="text" th:field="*{userProfile.lastName}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{email}">Email</label>
<input class="form-control" type="text" th:field="*{email}"/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="checkbox">
<button type="submit" class="btn btn-success" th:text="#{add_employee}">
Add employee
</button>
</div>
</div>
</div>
</form>
User entity:
#Entity
#Table(name="`user`")
public class User extends BaseModel {
#Column(unique = true, nullable = false, length = 45)
private String email;
#Column(nullable = false, length = 60)
private String password;
#Column
private String name;
#Column
private boolean enabled;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "user_role",
joinColumns = {#JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {#JoinColumn(name = "role_id", nullable = false)}
)
private Collection<Role> roles = new HashSet<Role>();
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "user_position",
joinColumns = {#JoinColumn(name = "user_id", nullable = false)},
inverseJoinColumns = {#JoinColumn(name = "position_id", nullable = false)}
)
private Collection<Position> positions = new HashSet<Position>();
public User() {
}
public User(String email, String password, boolean enabled) {
this.email = email;
this.password = password;
this.enabled = enabled;
}
public User(String email, String password, boolean enabled, Set<Role> roles) {
this.email = email;
this.password = password;
this.enabled = enabled;
this.roles = roles;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<Position> getPositions() {
return positions;
}
private void setPositions(Collection<Position> positions) {
this.positions = positions;
}
public boolean addPosition(Position position) {
return positions.add(position);
}
public boolean removePosition(Position position) {
return positions.remove(position);
}
public Collection<Role> getRoles() {
return roles;
}
private void setRoles(Collection<Role> roles) {
this.roles = roles;
}
public boolean addRole(Role role) {
return roles.add(role);
}
public boolean removeRole(Role role) {
return roles.remove(role);
}
#Override
public String toString() {
return User.class + " - id: " + getId().toString() + ", email: " + getEmail();
}
}
I have read somewhere that I have to create equals() and hashCode(), so I did it in my Position Entity.
public boolean equals(Position position) {
return this.getId() == position.getId();
}
public int hashCode(){
return this.getId().hashCode() ;
}
Here are data sent by post method:
And here is my result:
My spring version: 4.1.6.RELEASE
thymeleaf-spring4 version: 2.1.4.RELEASE
thymeleaf-layout-dialect version: 1.2.8
O course I wish positions to were HashCode with one element of object Position with id = 2.
Could you help me? What I am doing wrong?
It's because you're using ${position.id} for your option value. This means spring can't work out the relationship between the id used in the value and the actual Position objects. Try just ${position} for your value and it should work:
<select th:field="*{positions}" class="form-control">
<option th:each="position : *{positions}"
th:value="${position}"
th:text="${position.name}">Wireframe
</option>
</select>
(Make sure you've implemented hashCode and equals on your Position class)
If that still doesn't work you might have to implement a Formatter for Position, to make the conversion explicit. See this example thymeleafexamples-selectmultiple.
I had similar problem that I resolved by adding Formatter class and adding Formatter to the configuration of the MVC:
#Override
protected void addFormatters(FormatterRegistry registry){
registry.addFormatter(new PositionFormater());
...
}
and Position class formatter should look something like this:
PositionFormatter:
public class PositionFormatter implements Formatter<Position>{
/** String representing null. */
private static final String NULL_REPRESENTATION = "null";
#Resource
private PositionRepository positionRepository;
public PositionFormatter() {
super();
}
#Override
public String print(Position position, Locale locale) {
if(position.equals(NULL_REPRESENTATION)){
return null;
}
try {
Position newPosition = new Position();
newPosition.setId(position.getId());
return newPosition.getId().toString();
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + position + "` to a valid id");
}
}
#Override
public Position parse(String text, Locale locale) throws ParseException {
if (text.equals(NULL_REPRESENTATION)) {
return null;
}
try {
Long id = Long.parseLong(text);
Position position = new Position();
position.setId(id);
return position;
} catch (NumberFormatException e) {
throw new RuntimeException("Failed to convert `" + text + "` to valid Position");
}
}
}
In my case, these two solved all of the problems. I have several formatters, all I do is make one and add it to the config file (WebMVCConfig in my case)
Check my original post where I resolved this problem
Thanks Guys for answering my question. You help me a lot. Unfortunately I have to disagree with you in one thing. You have shown me example with:
newPosition.setId(position.getId());
The same example was in Andrew github repository. I think that this is bad practice to use setId() method. So I will present my solution and I will wait for some comments before I will mark it as an answer.
WebMvcConfig Class
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = "com.smartintranet")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#PersistenceContext
private EntityManager entityManager;
// (....rest of the methods.......)
#Override
public void addFormatters(FormatterRegistry formatterRegistry) {
formatterRegistry.addFormatter(new PositionFormatter(entityManager));
}
}
PositionFormatter class
public class PositionFormatter implements Formatter<Position> {
private EntityManager entityManager;
public PositionFormatter(EntityManager entityManager) {
this.entityManager = entityManager;
}
public String print(Position position, Locale locale) {
if(position.getId() == null){
return "";
}
return position.getId().toString();
}
public Position parse(String id, Locale locale) throws ParseException {
return entityManager.getReference(Position.class, Long.parseLong(id));
}
}
employeeForm.html
<div class="col-md-6">
<label th:text="#{position}">Position</label>
<select th:field="*{position}" class="form-control">
<option th:each="position : ${allPositions}"
th:value="${position.id}"
th:text="${position.name}">Wireframe
</option>
</select>
</div>
And last one, EmployeeController Class
#Controller
public class EmployeeController extends AbstractCrudController {
// (...rest of dependency and methods....)
#Transactional
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.GET)
public String createNewEmployee(Model model) {
prepareEmployeeForm(model);
return "crud/employee/create";
}
#Transactional
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, #ModelAttribute("employee") Employee employee, BindingResult result) {
if (!result.hasErrors()) {
// Look here it is important line!
entityManager.merge(employee.getUser());
}
prepareEmployeeForm(model);
return "crud/employee/create";
}
}
It is my solution. What is bad here? I think that line:
entityManager.merge(employee.getUser());
I can't use here:
userRepository.save(employee.getUser());
Because Position entity is detached, and when I use save method it runs in this situation em.persist() so I ran manually em.merge(). I know that this code is not perfect but I think that this solution is better then use setId(). I will be grateful for constructive critic.
One more time thanks Andrew and Blejzer for help without you I would not do it. I have marked yours answer as useful.
I am trying to learn how to build an application using Spring MVC and Hibernate. Currently I am stuck on inserting checkbox values into MySQL database.
My Database Table Structure is like following:
id name interest
When I fill up my form and hit submit I get this error message :
root cause
java.sql.SQLException: Incorrect string value: '\xAC\xED\x00\x05ur...' for column 'interest' at row 1
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1084)
com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4232)
I am trying to insert values in my table so that after insertion it looks like following:
id name interest
1 Steve PHP
2 Steve Java
3 Wuagh C#
4 Wuagh PHP
Could you please tell me how to achieve this? And If possible could you also tell me how can I achieve this as well ?
id name interest
1 Steve PHP, Java
2 Wuagh C#, PHP
Please see my codes below
My Form:
<c:url var="action" value="/register" ></c:url>
<form:form action="${action}" modelAttribute="subscriber" method="POST" >
<div>
<label>Name</label>
<form:input path="name"/>
<form:errors path="name" cssClass="error"/>
</div>
<div>
<label>Interests</label>
<form:checkboxes path="interest" items="${records.interests}"/>
</div>
<input type="submit" value="Submit">
</form:form>
Controller:
package com.spring.org;
#Controller
public class HomeController {
#Autowired
private SubscriberService subService;
#RequestMapping(value="/register", method= RequestMethod.GET)
public ModelAndView RegistrationForm(#ModelAttribute Subscriber subscriber, BindingResult result)
{
HashMap<Integer, String> interest = new HashMap<Integer, String>();
interest.put(1,"Java");
interest.put(2,"PHP");
interest.put(3, "C#");
return new ModelAndView("regForm", "records", interest);
}
#RequestMapping(value="/register", method= RequestMethod.POST)
public ModelAndView RegistrationFormSubmit(#ModelAttribute("subscriber") #Valid Subscriber subscriber, BindingResult result)
{
if (result.hasErrors()) {
return new ModelAndView("regForm");
}
else
{
subService.addSubscriber(subscriber);
return new ModelAndView("redirect:/showList");
}
}
}
Model - Subscriber
#Entity
#Table(name = "PERSON", schema = "java2")
public class Subscriber {
#Id
#Column(name="ID")
#GeneratedValue
private int id;
#NotEmpty(message = "Please enter your Name.")
private String name;
private String[] interest;
public String getName() {return name;}
public void setName(String name) { this.name = name; }
public String[] getInterest() { return interest; }
public void setInterest(String[] interest) { this.interest = interest; }
}
SubscribeService Implementation :
#Service
public class SubscriberServiceImpl implements SubscriberService{
#Autowired
private SubscriberDao subsDao ;
#Override
public void addSubscriber(Subscriber subscriber) {
subsDao.addSubscriber(subscriber);
}
}
SubscriberDao Implementation :
#Repository
public class SubscriberDaoImpl implements SubscriberDao {
#Autowired
private SessionFactory sessionFactory ;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
#Override
public void addSubscriber(Subscriber subscriber) {
getSessionFactory().openSession().save(subscriber);
}
}