Can I add any object to Model in Spring MVC? - spring

I wanted to add a bean to model in Spring MVC Controller. But, validator exception is thrown:
Illegalstate exception.
Can anyone guide me to submit a form and display the content which I get after form submission? In this case, I need to use a bean to display all my information in view.
Like:
model.addAttribute("simple", new Student());
But, I am keep getting IllegaStateException from validator.
Download:
https://sites.google.com/site/jimjicky/SpringFormValidation.rar?attredirects=0&d=1
Controller:
#Controller
public class EmployeeController {
private static final Logger logger = LoggerFactory
.getLogger(EmployeeController.class);
private Map<Integer, Employee> emps = null;
#Autowired
#Qualifier("employeeValidator")
private Validator validator;
#InitBinder
private void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
public EmployeeController() {
emps = new HashMap<Integer, Employee>();
}
#ModelAttribute("employee")
public Employee createEmployeeModel() {
// ModelAttribute value should be same as used in the empSave.jsp
return new Employee();
}
#ModelAttribute("student")
public Student createStudentModel() {
// ModelAttribute value should be same as used in the empSave.jsp
return new Student();
}
#RequestMapping(value = "/emp/save", method = RequestMethod.GET)
public String saveEmployeePage(Model model) {
logger.info("Returning empSave.jsp page");
return "empSave";
}
#RequestMapping(value = "/emp/save.do", method = RequestMethod.POST)
public String saveEmployeeAction(
#ModelAttribute("employee") #Validated Employee employee,#ModelAttribute("student")Student student,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
logger.info("Returning empSave.jsp page");
return "empSave";
}
logger.info("Returning empSaveSuccess.jsp page");
model.addAttribute("emp", employee);
model.addAttribute("student", createStudentModel());
emps.put(employee.getId(), employee);
return "empSaveSuccess";
}
}
Validator:
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.journaldev.spring.form.model.Employee;
public class EmployeeFormValidator implements Validator {
//which objects can be validated by this validator
#Override
public boolean supports(Class<?> paramClass) {
return Employee.class.equals(paramClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "id.required");
Employee emp = (Employee) obj;
if(emp.getId() <=0){
errors.rejectValue("id", "negativeValue", new Object[]{"'id'"}, "id can't be negative");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "role", "role.required");
}
}
Stack Trace
java.lang.IllegalStateException: Invalid target for Validator [com.journaldev.spring.form.validator.EmployeeFormValidator#1625cd8]: com.journaldev.spring.form.model.Student#bd8550
org.springframework.validation.DataBinder.assertValidators(DataBinder.java:495)
org.springframework.validation.DataBinder.setValidator(DataBinder.java:486)
com.journaldev.spring.form.controllers.EmployeeController.initBinder(EmployeeController.java:38)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
org.springframework.web.method.annotation.InitBinderDataBinderFactory.initBinder(InitBinderDataBinderFactory.java:62)
org.springframework.web.bind.support.DefaultDataBinderFactory.createBinder(DefaultDataBinderFactory.java:53)
org.springframework.web.method.annotation.ModelFactory.updateBindingResult(ModelFactory.java:222)
org.springframework.web.method.annotation.ModelFactory.updateModel(ModelFactory.java:206)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.getModelAndView(RequestMappingHandlerAdapter.java:852)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:755)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:690)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)

Based on the information provided try changing your InitBinder to following:
#InitBinder("employee")
private void initBinder(WebDataBinder binder) {
binder.setValidator(validator);
}
The reason this works is because of the value provided, in this case the value being "employee".
public abstract String[] value
The names of command/form attributes and/or request parameters that this init-binder method is supposed to apply to.
In your form you have multiple objects, without advising the #InitBinder what object to validate it attempted to validate your Student object as well, thus failing as it didn't meet the class requirements.
By specifying "employee" it basically ensured it only applied the validation against the Employee object.

Related

Spring validator fails looking up value of class level constraint

I'm getting an error with JSR 303 class level validation and spring and I'd like to know if I am setting things up in the correct way.
I'm using validation in spring (4.2.6.RELEASE) using hibernate-validator (5.2.4.Final) with a typical setup inside a spring controller like:
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public SomeDto update(#PathVariable Integer id, #Valid #RequestBody SomeDto someDto) {
...
return someDto;
}
This works fine with most setups, but when the target of the validation includes a set of objects that are annotated with a Class level validation the SpringValidatorAdaptor fails when trying to lookup the original value
The following code illustrates the problem:
Target class to validate:
public class Base {
#Valid
Set<MyClass> myClassSet = new HashSet<>();
public Set<MyClass> getMyClassSet() {
return myClassSet;
}
Class with class level validation annotation:
#CheckMyClass
public class MyClass {
String a;
String b;
public MyClass(final String a, final String b) {
this.a = a;
this.b = b;
}
}
Constraint:
#Target({ TYPE, ANNOTATION_TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = CheckMyClassValidator.class)
#Documented
public #interface CheckMyClass {
String message() default "Invalid class";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Validator:
public class CheckMyClassValidator implements ConstraintValidator<CheckMyClass, MyClass> {
#Override
public void initialize(final CheckMyClass constraintAnnotation) {
}
#Override
public boolean isValid(final MyClass value, final ConstraintValidatorContext context) {
return false; // want it to fail for testing purposes
}
}
Test class:
#SpringBootApplication
public class SpringValidationTest {
#Bean
public org.springframework.validation.Validator validator() {
return new org.springframework.validation.beanvalidation.LocalValidatorFactoryBean();
}
private void doStandardValidation() {
Base base = createBase();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Base>> violations = validator.validate(base);
for (ConstraintViolation<?> violation : violations) {
System.out.println(violation.getMessage());
}
}
private Base createBase() {
Base base = new Base();
base.getMyClassSet().add(new MyClass("a1", "b1"));
// base.getMyClassSet().add(new MyClass("a2", "b2"));
return base;
}
#PostConstruct
private void doSpringValidation() {
Base base = createBase();
org.springframework.validation.Validator validator = validator();
DataBinder binder = new DataBinder(base);
binder.setValidator(validator);
binder.validate();
BindingResult results = binder.getBindingResult();
for (ObjectError error : results.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
public static void main(String[] args) {
(new SpringValidationTest()).doStandardValidation();
System.out.println();
ApplicationContext applicationContext = SpringApplication.run(SpringValidationTest.class);
}
}
The standard validation works fine, but when it is wrapped by the spring validator (as in the typical controller setup) it ends up with an exception (as below) trying to lookup the value of the property:
Caused by: java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_45]
at java.lang.Integer.parseInt(Integer.java:592) ~[na:1.8.0_45]
at java.lang.Integer.parseInt(Integer.java:615) ~[na:1.8.0_45]
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:657) ~[spring-beans-4.2.6.RELEASE.jar:4.2.6.RELEASE]
... 37 common frames omitted

How to validate Spring MVC #PathVariable values?

For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?
For example:
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") String customerNumber) {
...
}
Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?
Thanks.
Spring does not support #javax.validation.Valid on #PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.
Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.
You can use like this:
use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.
*
* Variant of JSR-303's {#link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
step.1 init ValidationConfig
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
return processor;
}
}
step.2 Add #Validated to your controller handler class, Like:
#RequestMapping(value = "poo/foo")
#Validated
public class FooController {
...
}
step.3 Add validators to your handler method:
#RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public ResponseEntity<Foo> delete(
#PathVariable("id") #Size(min = 1) #CustomerValidator int id) throws RestException {
// do something
return new ResponseEntity(HttpStatus.OK);
}
final step. Add exception resolver to your context:
#Component
public class BindExceptionResolver implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex.getClass().equals(BindException.class)) {
BindException exception = (BindException) ex;
List<FieldError> fieldErrors = exception.getFieldErrors();
return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
}
}
}
The solution is simple:
#GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(#PathVariable(value = "hash", required = false) String historyHash)
{
// Accepted requests: either "/" or "/{40 character long hash}"
}
And yes, PathVariables are ment to be validated, like any user input.
Instead of using #PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:
#RestController
#RequestMapping("/user")
public class UserController {
#GetMapping("/{id}")
public void get(#Valid GetDto dto) {
// dto.getId() is the path variable
}
}
And the bean contains the actual validation rules:
#Data
public class GetDto {
#Min(1) #Max(99)
private long id;
}
Make sure that your path variables ({id}) correspond to the bean fields (id);
#PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:
a bug generated a bad url (an href in jsp for example). No #Valid is
needed and no message is needed, just fix the code;
"the user" is manipulating the url.
Again, no #Valid is needed, no meaningful message to the user should
be given.
In both cases just leave an exception bubble up until it is catched by
the usual Spring ExceptionHandlers in order to generate a nice
error page or a meaningful json response indicating the error. In
order to get this result you can do some validation using custom editors.
Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)
public class CustomerNumber implements CharSequence {
private String customerNumber;
public CustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
#Override
public String toString() {
return customerNumber == null ? null : customerNumber.toString();
}
#Override
public int length() {
return customerNumber.length();
}
#Override
public char charAt(int index) {
return customerNumber.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return customerNumber.subSequence(start, end);
}
#Override
public boolean equals(Object obj) {
return customerNumber.equals(obj);
}
#Override
public int hashCode() {
return customerNumber.hashCode();
}
}
Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)
public class CustomerNumberEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
setValue(new CustomerNumber(text));
} else {
throw new IllegalArgumentException();
// you could also subclass and throw IllegalArgumentException
// in order to manage a more detailed error message
}
}
#Override
public String getAsText() {
return ((CustomerNumber) this.getValue()).toString();
}
}
Register the editor in the Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
// ... other editors
}
Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") CustomerNumber customerNumber) {
...
}
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations?
To validate path variable you should use this approach Problem validating #PathVariable url on spring 3 mvc
Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below
#RequestMapping(value = "/number")
#ResponseBody
public ResponseObject searchByNumber() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
}

spring mvc process object before #valid is applied

I have a spring mvc controller like the following
#RequestMapping(value="/new", method=RequestMethod.POST)
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal)
{
if(bindingResult.hasErrors()) {
return "booking/edit";
}
//Store Booking in db...
...
The problem is the Booking object i get from the POST is constructed by Spring, but one of the properties required by the validator cannot be populated, as the property is not present in the form. So my question is is there a way for me to intercept the Booking before it gets processed by the #Valid tag handler to add this required property?
Cheers!
NFV
There are 2 ways to modify the model attribute object before the #Valid will trigger:
Remove #Valid and autowire the validator and manually trigger the validator:
class MyController {
private final Validator validator;
class MyController(Validator validator) {
this.validator = validator;
}
#PostMapping("/new")
public String createBooking(Booking booking, BindingResult bindingResult, Model model, Principal principal) {
// edit booking here
validator.validate(booking, result)
// original method body here
}
}
Decorate the default validator and pre-process the booking object inside the decorated validator.
class MyController {
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new PreProcessBookingValidator(binder.getValidator()));
}
#PostMapping("/new")
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal) {
...
}
private static class PreProcessBookingValidator implements Validator {
private final Validator validator;
public PreProcessBookingValidator(Validator validator) {
this.validator = validator;
}
#Override
public boolean supports(#Nonnull Class<?> clazz) {
return validator.supports(clazz);
}
#Override
public void validate(#Nonnull Object target, #Nonnull Errors errors) {
if (target instanceof Booking) {
Booking booking = (Booking) target;
// manipulate booking here
}
validator.validate(target, errors);
}
}
}
(This second tip is what I picked up from https://github.com/spring-projects/spring-framework/issues/11103)
I'm not sure I understand the question, but it sounds like you want to set a field in the command object to a value before your command is bound by the form submission. If so, you can add a method in your controller as follows...
#ModelAttribute
public Booking getBooking() {
Booking booking = new Booking();
booking.setMyRequiredProperty("some value");
return booking;
}
Hope that helps

Server side Validation not working properly

I am configuring server side validation for my form.My problem is that when the control comes in the Areavalidator class
#Override
public boolean supports(Class<?> clazz) {
return Area.class.isAssignableFrom(clazz);
}
from the above method the control again back to the controller class and in the error set it shows zero error.My question is that why it is not entering in the method where I am doing my validation stuff.
#Override
public void validate(Object target, Errors errors) {
Area object = (Area)target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "areaName",
"label.areaNameRequired");
if(object.getAreaCode().length()==0)
{
{
errors.rejectValue("areaCode", "label.areaCode", null);
}
}
}
The code in my controller class for validation
#Autowired
private AreaValidator areaValidator;
#InitBinder("area")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(areaValidator);
}
#RequestMapping(value = "/saveGridArea", method = RequestMethod.POST)
public String saveCountry(#ModelAttribute #Valid Area area,ModelMap map,BindingResult error) {
if (error.hasErrors()) {
return "area";
}
It's because when you do #Valid, it is expected to have the corresponding BindingResult right next to the modelAttribute:
Here, your ModelMap is in between, making it impossible for the framework to associate the linked/associated errors to the modelAttribute .
You just need to change the order of your variable of the method.
Try this and it should work:
#RequestMapping(value = "/saveGridArea", method = RequestMethod.POST)
public String saveCountry(#ModelAttribute #Valid Area area,BindingResult error, ModelMap map){
...
}

Lazy loading of child throwing session error

I'm the following error when calling purchaseService.updatePurchase(purchase) inside my TagController:
SEVERE: Servlet.service() for servlet [PurchaseAPIServer] in context with path [/PurchaseAPIServer] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed] with root cause
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.app.model.Purchase.tags, no session or session was closed
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:383)
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:375)
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:368)
at org.hibernate.collection.PersistentSet.add(PersistentSet.java:212)
at com.app.model.Purchase.addTags(Purchase.java:207)
at com.app.controller.TagController.createAll(TagController.java:79)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:225)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:169)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:999)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:565)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:309)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
TagController:
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchase.addTags(tags);
purchaseService.updatePurchase(purchase);
}
Purchase:
#Entity
#XmlRootElement
public class Purchase implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 6603477834338392140L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(mappedBy = "purchase", fetch = FetchType.LAZY, cascade={CascadeType.ALL})
private Set<Tag> tags;
#JsonIgnore
public Set<Tag> getTags()
{
if (tags == null)
{
tags = new LinkedHashSet<Tag>();
}
return tags;
}
public void setTags(Set<Tag> tags)
{
this.tags = tags;
}
public void addTag(Tag tag)
{
tag.setPurchase(this);
this.tags.add(tag);
}
public void addTags(Set<Tag> tags)
{
Iterator<Tag> it = tags.iterator();
while (it.hasNext())
{
Tag tag = it.next();
tag.setPurchase(this);
this.tags.add(tag);
}
}
...
}
Tag:
#Entity
#XmlRootElement
public class Tag implements Serializable
{
/**
*
*/
private static final long serialVersionUID = 5165922776051697002L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumns({#JoinColumn(name = "PURCHASEID", referencedColumnName = "ID")})
private Purchase purchase;
#JsonIgnore
public Purchase getPurchase()
{
return purchase;
}
public void setPurchase(Purchase purchase)
{
this.purchase = purchase;
}
}
PurchaseService:
#Service
public class PurchaseService implements IPurchaseService
{
#Autowired
private IPurchaseDAO purchaseDAO;
public PurchaseService()
{
}
#Transactional
public List<Purchase> getAll()
{
return purchaseDAO.findAll();
}
#Transactional
public Purchase getById(Long id)
{
return purchaseDAO.findOne(id);
}
#Transactional
public void addPurchase(Purchase purchase)
{
purchaseDAO.save(purchase);
}
#Transactional
public void updatePurchase(Purchase purchase)
{
purchaseDAO.update(purchase);
}
}
TagService:
#Service
public class TagService implements ITagService
{
#Autowired
private ITagDAO tagDAO;
public TagService()
{
}
#Transactional
public List<Tag> getAll()
{
return tagDAO.findAll();
}
#Transactional
public Tag getById(Long id)
{
return tagDAO.findOne(id);
}
#Transactional
public void addTag(Tag tag)
{
tagDAO.save(tag);
}
#Transactional
public void updateTag(Tag tag)
{
tagDAO.update(tag);
}
}
Any ideas on how I can fix this? (I want to avoid using EAGER loading).
Do I need to setup some form of session management for transactions?
Thanks
Updates based on JB suggestions
TagController
#RequestMapping(value = "purchases/{purchaseId}/tags", method = RequestMethod.POST, params = "manyTags")
#ResponseStatus(HttpStatus.CREATED)
public void createAll(#PathVariable("purchaseId") final Long purchaseId, #RequestBody final Tag[] entities)
{
Purchase purchase = purchaseService.getById(purchaseId);
// Validation
RestPreconditions.checkRequestElementNotNull(purchase);
RestPreconditions.checkRequestElementIsNumeric(purchaseId);
Set<Tag> tags = new HashSet<Tag>(Arrays.asList(entities));
purchaseService.addTagsToPurchase(purchaseId, tags);
}
PurchaseService
#Transactional
public void addTagsToPurchase(Long purchaseId, Set<Tag> tags)
{
Purchase p = purchaseDAO.findOne(purchaseId);
p.addTags(tags);
}
You load the Purchase without initializing its tags collection and then, in your controller, once the transaction is committed and the session closed, you're adding a tag to this non-initialized collection. So obviously, this exception is thrown.
You have two possibilities:
Load the tags with the Purchase, either by calling Hibernate.initialize(purchase.getTags()), or by executing a JPQL query that loads the tag along with its tags, in a single query (select distinct p from Purchase p left join fetch p.tags).
Instead of having a service class that offers noting more than the DAO, add a real service method addTagsToPurchase(Long purchaseId, Set<Tag> tags) which reloads the purchase and adds the tags to this purchase. This is in fact the service method needed by your controller.
I of course prefer the second solution by far:
it shows why a service layer is useful,
it removes business code from the presentation layer to put it in the service layer,
it's more efficient
it's safer because it uses attached entities, and thus avoids the kind of exception you're having.
In general, if your controller, to execute a single atomic action, needs several calls to the service layer, it means that the service layer doesn't offer the needed functionality, and that the service is implemented in the presentation layer instead.

Resources