I am using Spring MVC 2.5, and I am trying to get a JSTL form object to load from a GET request. I have Hibernate POJOs as my backing objects.
There is one page directing to another page with a class id (row primary key) in the request. The request looks like "newpage.htm?name=RowId". This is going into a page with a form backing object,
The newpage above, loads the fields of the object into editable fields, populated with the existing values of the row. The idea is, that you should be able to edit these fields and then persist them back into the database.
The view of this page looks something like this
<form:form commandName="thingie">
<span>Name:</span>
<span><form:input path="name" /></span>
<br/>
<span>Scheme:</span>
<span><form:input path="scheme" /></span>
<br/>
<span>Url:</span>
<span><form:input path="url" /></span>
<br/>
<span>Enabled:</span>
<span><form:checkbox path="enabled"/></span>
<br/>
<input type="submit" value="Save Changes" />
</form:form>
The controller has this in it,
public class thingieDetailController extends SimpleFormController {
public thingieDetailController() {
setCommandClass(Thingie.class);
setCommandName("thingie");
}
#Override
protected Object formBackingObject(HttpServletRequest request) throws Exception {
Thingie thingieForm = (Thingie) super.formBackingObject(request);
//This output is always null, as the ID is not being set properly
logger.debug("thingieForm.getName(): [" + thingieForm.getName() + "]");
//thingieForm.setName(request.getParameter("name"));
SimpleDAO.loadThingie(thingieForm);
return thingieForm;
}
#Override
protected void doSubmitAction(Object command) throws Exception {
Thingie thingie = (Thingie) command;
SimpleDAO.saveThingie(thingie);
}
}
As you can see from the commented code, I've tried manually setting the object id (name is this case) from the request. However Hibernate complains about the object being desynched when I try and persist the data in the form.
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
This error seems to do something to the entire session, which stops working for my entire web application, continually throwing the Stale Object State Exception seen above.
If anyone familiar with Spring MVC can help me with this or suggest a workaround, I would really appreciate it.
EDIT:
Session factory code.
private static final SessionFactory sessionFactory;
private static final Configuration configuration = new Configuration().configure();
static {
try {
// Create the SessionFactory from standard (hibernate.cfg.xml)
// config file.
sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
} catch (Throwable ex) {
// Log the exception.
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
One of the major flaws with using Spring MVC + hibernate is that the natural approach is to use the hibernate domain object as the backing object for the form. Spring will bind anything in the request based on name by DEFAULT. This inadvertently includes things like ID or name (usually the primary key) or other hibernate managed properties being set. This also makes you vulnerable to form injection.
In order to be secure under this scenario you must use something like:
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
throws Exception {
String[] allowedFields = {"name", "birthday"}
binder.setAllowedFields(allowedFields);
}
and EXPLICITLY set the ALLOWED fields to only those in your form, and exclude the primary key or you will end up with a mess!!!
To answer your immediate question, the problem you are having with Hibernate has to do with the following sequence of events:
One Hibernate session is opened (let's call it session A) in formBackingObject
Using session A, you load a Thingie object in formBackingObject
You return the Thingie object as the result of formBackingObject
When you return the Thingie object, session A is closed, but Thingie is still linked to it
When doSubmitAction is called, the same instance of the Thingie backing object is passed as the command
A new Hibernate session (call it session B) is opened
You attempt to save the Thingie object (originally opened with session A) using session B
Hibernate doesn't know anything about session A at this point, because it's closed, so you get an error. The good news is that you shouldn't be doing it that way, and the correct way will bypass that error completely.
The formBackingObject method is used to populate a form's command object with data, prior to showing the form. Based on your updated question, it sounds like you are simply trying to display a form populated with information from a given database row, and update that database row when the form is submitted.
It looks like you already have a model class for your record; I'll call that the Record class in this answer). You also have DAO for the Record class, which I will call RecordDao. Finally, you need a UpdateRecordCommand class that will be your backing object. The UpdateRecordCommand should be defined with the following fields and setters/getters:
public class UpdateRecordCommand {
// Row ID of the record we want to update
private int rowId;
// New name
private int String name;
// New scheme
private int String scheme;
// New URL
private int String url;
// New enabled flag
private int boolean enabled;
// Getters and setters left out for brevity
}
Then define your form using the following code:
<form:form commandName="update">
<span>Name:</span>
<span><form:input path="name" /></span><br/>
<span>Scheme:</span>
<span><form:input path="scheme" /></span><br/>
<span>Url:</span>
<span><form:input path="url" /></span><br/>
<span>Enabled:</span>
<span><form:checkbox path="enabled"/></span><br/>
<form:hidden path="rowId"/>
<input type="submit" value="Save Changes" />
</form:form>
Now you define your form controller, which will populate the form in formBackingObject and process the update request in doSubmitAction.
public class UpdateRecordController extends SimpleFormController {
private RecordDao recordDao;
// Setter and getter for recordDao left out for brevity
public UpdateRecordController() {
setCommandClass(UpdateRecordCommand.class);
setCommandName("update");
}
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
// Use one of Spring's utility classes to cleanly fetch the rowId
int rowId = ServletRequestUtils.getIntParameter(request, "rowId");
// Load the record based on the rowId paramrter, using your DAO
Record record = recordDao.load(rowId);
// Populate the update command with information from the record
UpdateRecordCommand command = new UpdateRecordCommand();
command.setRowId(rowId);
command.setName(record.getName());
command.setScheme(record.getScheme());
command.setUrl(record.getUrl());
command.setEnabled(record.getEnabled());
// Returning this will pre-populate the form fields
return command;
}
#Override
protected void doSubmitAction(Object command) throws Exception {
// Load the record based on the rowId in the update command
UpdateRecordCommand update = (UpdateRecordCommand) command;
Record record = recordDao.load(update.getRowId());
// Update the object we loaded from the data store
record.setName(update.getName());
record.setScheme(update.getScheme());
record.setUrl(update.getUrl());
record.setEnabled(update.setEnaled());
// Finally, persist the data using the DAO
recordDao.save(record);
}
}
Your issue may be related to Detached Objects. Because your DAO has been modified outside a Hibernate session, you need to reattach the object to the Hibernate session before saving. You can do this either by explicitly bringing the object into the session before saving using Merge() or update(). Experiment with both, and read the documentation for those actions, as they have different effects depending on the structure of your data objects.
What was going on is that the ?name=rowId was somehow messing up the form post. Once I changed that to a name that didn't reflect a parameter in the object, everything worked fine. No change to the DAO or controller code necessary.
Thanks to everyone for your answers. It helped me narrow down what was going on.
Related
I have implemented by project using Spring-Data-Rest. I am trying to do an update on an existing record in a table. But when I try to send only a few fields instead of all the fields(present in Entity class) through my request, Spring-Data-Rest thinking I am sending null/empty values. Finally when I go and see the database the fields which I am not sending through my request are overridden with null/empty values. So my understanding is that even though I am not sending these values, spring data rest sees them in the Entity class and sending these values as null/empty. My question here is, is there a way to disable the fields when doing UPDATE that I am not sending through the request. Appreciate you are any help.
Update: I was using PUT method. After reading the comments, I changed it to PATCH and its working perfectly now. Appreciate all the help
Before update, load object from database, using jpa method findById return object call target.
Then copy all fields that not null/empty from object-want-to-update to target, finally save the target object.
This is code example:
public void update(Object objectWantToUpdate) {
Object target = repository.findById(objectWantToUpdate.getId());
copyNonNullProperties(objectWantToUpdate, target);
repository.save(target);
}
public void copyNonNullProperties(Object source, Object target) {
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
public String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] propDesList = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<String>();
for(PropertyDescriptor propDesc : propDesList) {
Object srcValue = src.getPropertyValue(propDesc.getName());
if (srcValue == null) {
emptyNames.add(propDesc.getName());
}
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
As some of the comments pointed out using PATCH instead of PUT resolved the issue. Appreciate all the inputs. The following is from Spring Data Rest Documentation:
"The PUT method replaces the state of the target resource with the supplied request body.
The PATCH method is similar to the PUT method but partially updates the resources state."
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.hiding-repository-crud-methods
Also, I like #Tran Quoc Vu answer but not implementing it for now since I dont have to use custom controller. If there is some logic(ex: validation) involved when updating the entity, I am in favor of using the custom controller.
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
I have a service (which I for some reason call controller) that is injected into the Jersey resource method.
#Named
#Transactional
public class DocCtrl {
...
public void changeDocState(List<String> uuids, EDocState state, String shreddingCode) throws DatabaseException, WebserviceException, RepositoryException, ExtensionException, LockException, AccessDeniedException, PathNotFoundException, UnknowException {
List<Document2> documents = doc2DAO.getManyByUUIDs(uuids);
for (Document2 doc : documents) {
if (EDocState.SOFT_DEL == state) {
computeShreddingFor(doc, shreddingCode); //here the state change happens and it is persisted to db
}
if (EDocState.ACTIVE == state)
unscheduleShredding(doc);
}
}
}
doc2DAO.getManyByUUIDs(uuids); gets an Entity object from the database.
#Repository
public class Doc2DAO {
#PersistenceContext(name = Vedantas.PU_NAME, type = PersistenceContextType.EXTENDED)
private EntityManager entityManager;
public List<Document2> getManyByUUIDs(List<String> uuids) {
if (uuids.isEmpty())
uuids.add("-3");
TypedQuery<Document2> query = entityManager.createNamedQuery("getManyByUUIDs", Document2.class);
query.setParameter("uuids", uuids);
return query.getResultList();
}
}
However When I do second request to my API, I see state of this entity object unchanged, that means the same as before the logic above occoured.
In DB there is still changed status.
After the api service restart, I will get the entity in the correct state.
As I understand it, Hibernate uses it's L2 cache for the managed objects.
So can you, please point me to what I am doing wrong here? Obviously, I need to get cached entity with the changed state without service restart and I would like to keep entities attached to the persistence context for the performance reasons.
Now, can you tell me what I am
In the logic I am making some changes to this object. After the completition of the changeDocState method, the state is properly changed and persisted in the database.
Thanks for the answers;
in My jsp I am using spring form tags to bind and update data.
my scenario is to show default values when user enters the page. these default values should come from database depending on some conditions.
But when user edits the data and submit, I want to save them in databse without updating the default values.
Any suggestion is greatly appreciated!
Standard approach to a form page is to have a controller with two methods, one for GET and one for POST. You'll also need an object to bind to the form.
The method that handles the GET sets up the bind object and puts it in the model, then returns the view for the form page. The bind object here can be created with default values which you can get from anywhere. Your bind object will probably have some key, like an ID. The default one can have a zero/default key.
The method that handles the POST takes the bind object as a parameter. It probably validates the values then inserts into the database. Its likely the database will generate the key/id.
Here's an example;
#Controller
public class PersonController {
#RequestMapping(value="/person.do", method=RequestMethod.GET)
public ModelAndView setup() {
ModelAndView response = new ModelAndView("person");
//Create default bind object, can get values
//from database if you like. Here they're just
//hard coded.
Person person = new Person();
person.setName("Joe Bloggs");
response.addObject("person", person);
return response;
}
#RequestMapping(value="/person.do", method=RequestMethod.POST)
public ModelAndView post(#ModelAttribute("person") Person person,
BindingResult result) {
Validator.validate(person, result);
if (result.hasErrors()) {
ModelAndView response = new ModelAndView("person");
response.addObject("person", person);
return response;
} else {
personDao.store(person);
}
return new ModelAndView("redirect:nextPage.do");
}
}
The form will be populated with any values you supply in the backing object. I don't understand the second part of your question.
I am using Spring SimpleFormController for my forms and for some reason it won't go to the onSubmit method
Here's my code:
public class CreateProjectController extends SimpleFormController {
ProjectDao projectDao;
public CreateProjectController() {
setCommandClass(Project.class);
setCommandName("Project");
setSessionForm(true);
}
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project project = projectDao.getProjectByOutsideId(id);
System.out.println("#formbacking object method");
System.out.println("the success view is "+getSuccessView());
return project;
}
#Override
protected ModelAndView onSubmit(Object command) throws Exception {
Project project = (Project) command;
System.out.println("this is the project title: "+project.getTitle());
System.out.println("the success view is "+getSuccessView());
projectDao.insert(project);
return new ModelAndView(getSuccessView());
}
I know because it prints "#formbacking object method" string but not the "the success view is..." string and the :"this is the pr..." string. I see "#formback.." string in the console but not the last two whenever I hit submit. I don't know where the problem is.
This is my jsp
<form:form method="POST" commandName="Project">
Name: <form:input path="title"/><br/>
Description: <form:input path="description"/><br/>
Link: <form:input path="url" disabled="true"/><br/>
Tags: <form:input path="tags"/><br/>
Assessors <form:input path="assessors"/><br/><br/>
<input type="submit" value="submit"/>
</form:form>
I am running on Google App Engine btw. Maybe the problem is there?
UPDATE: The problem seems to be with the formBackingObject method. When I removed it, the form now goes to the onSubmit when I click submit.
But I'd like to have values from of the command class from the database in my forms.
Another piece of code that doesn't work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle(title);
project.setUrl("projectUrl");
return project;
}
but this does work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle("projectTitle");
project.setUrl("projectUrl");
return project;
}
Now I am really confused. haha.
I was thinking along the same lines as axtavt. You are only going to have an id request parameter on updates, so you should add some code for creation forms:
FYI, formBackingObject requires a non-null object to be returned. To save some memory, you can have a final constant member variable that is the default return value. Your code satisfies this though since you're transferring objects, but I don't get why you're transferring data (creating an extra object) when you're not using a DTO. You could simply do this:
private final static Project PROJECT_INSTANCE = new Project();
static {
PROJECT_INSTANCE.setTitle("defaultProjectTitle");
}
#Override
protected Project formBackingObject(HttpServletRequest request) throws Exception {
String id = request.getParameter("id");
if(id == null || id.trim().length() == 0 || !id.matches("\\d+")) {
return PROJECT_INSTANCE;
}
return projectDao.getProjectByOutsideId(id);
}
You don't need a hidden id input field. You would use formBackingObject() for initializing the form input fields for updating (by navigating to page.jsp?id=111).
Look at the String id = request.getParameter("id");. There is no such field in your form, so probably you get an error there during submit process, maybe, getProjectByOutsideId returns null.
P.S. It's strange that your formBackingObject is executing when you press submit, it shouldn't if you really set setSessionForm(true).
Try turning the spring debugging up. It provides a lot of information, which can be helpful. Do this by editing the log4j.properties file.
log4j.logger.org.springframework=DEBUG
Have you added logging to make sure the formBackingObject is returning something?
System.out.println("#formbacking object method is returning: " + project);
It will make sure something is being returned. In general the formBackingObject should always return something.
EDIT:
Id is not being passed during submission in the snippet. Maybe it is during the load, e.g. /page.do?id=4, but it doesn't appear in the form.
Add <form:hidden path="id"/> to your form during on submit. Otherwise the id will not be a parameter and the getProjectByOutsideId will fail.