Is it possible to update only a subset of attributes on an entity using Spring MVC with JPA?
Asked Answered
D

3

17

I'm working with Spring Roo, using Spring MVC and JPA for persistence with a MySQL database. I'm very new to Spring MVC and Java in general but have worked with CakePHP and Rails.

I have a User entity that contains personal details in addition to a password. Something like this (excluding a lot of Roo-generated functionality in additional .aj files):

public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;

    @Column(name = "PASSWORD", length = 32)
    private String password;

    @Column(name = "FIRST_NAME", length = 25)
    private String firstName;

    @Column(name = "LAST_NAME", length = 25)
    private String lastName;

    @Column(name = "ADDRESS", length = 255)
    private String address;

    // The appropriate getters and setters
    ...
}

Then I have an edit action in my User controller that I created following conventions from Roo's auto-generated scaffolding:

@RequestMapping(value="/edit", method = RequestMethod.GET)
public String editForm(Model uiModel) {
    String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    uiModel.addAttribute("user", User.findUserByUsername(username).getSingleResult());
    return "account/edit";
}

And a JSPX view to render the form, again following Roo's conventions:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:field="urn:jsptagdir:/WEB-INF/tags/form/fields" xmlns:form="urn:jsptagdir:/WEB-INF/tags/form" xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0">
    <jsp:directive.page contentType="text/html;charset=UTF-8"/>
    <jsp:output omit-xml-declaration="yes"/>
    <form:update id="" label="Personal Details" modelAttribute="user" path="/account" versionField="none">
        <field:input id="" field="firstName" label="First Name" />
        <field:input id="" field="lastName" label="Last Name" />
        <field:textarea id="" field="address" label="Street Address" />
    </form:update>
</div>

I do not want the form to update the password, just the provided fields (first name, last name, and address).

The update action, again following Roo convention:

@RequestMapping(method = RequestMethod.PUT, produces = "text/html")
public String edit(@Valid User user, BindingResult bindingResult, Model uiModel, HttpServletRequest httpServletRequest) {
    if (bindingResult.hasErrors()) {
        uiModel.addAttribute("user", user);
        return "account/edit";
    }
    uiModel.asMap().clear();
    user.merge();
    return "redirect:/account";
}

The user object is updated perfectly, but the problem is that it overwrites the password field with null because it's not provided as an input in the form, and thus set to null in the User object passed to the form submit request handler. The problem doesn't show up with the Roo generated scaffolding because they provide form inputs for all of the columns. So I could add it as a hidden field but that doesn't sound like a good idea. And I get the feeling there's a much better way to do it...


TL;DR How can I update only the entity attributes provided in a form without overwriting the other attributes?

In other words, how can I make Spring/JPA generate the SQL

UPDATE user SET firstname=?, lastname=?, address=?

instead of

UPDATE user SET firstname=?, lastname=?, address=?, password=?

Code samples would be fantastic since I'm new to all of this :)


Thank you!


UPDATE: I was able to make it work using yglodt's suggestion, adding the following method to my User model:

@Transactional
public void mergeWithExistingAndUpdate() {
    final User existingUser = User.findUser(this.getId());

    existingUser.setFirstName(this.getFirstName());
    existingUser.setLastName(this.getLastName());
    existingUser.setAddress(this.getAddress());

    existingUser.flush();
}

and calling that from my controller action instead of user.merge():

user.mergeWithExistingAndUpdate();
Dispraise answered 5/2, 2013 at 2:16 Comment(1)
You mean with HIBERNATE. Other JPA providers do NOT add in all unchanged fields into the UPDATE clauseSanburn
S
15

I usually solve this in the service layer.

You can read the entity you want to update from the DB, and overwrite the attributes which you are getting from your form.

This way you change only the attributes you want.

Code example:

@Service
@Transactional
public class UserService {

    @Resource(name = "sessionFactory")
    private SessionFactory  sessionFactory;

    public void mergeWithExistingAndUpdate(final Person personFromPost) {

        Session session = sessionFactory.getCurrentSession();

        Person existingPerson = (Person) session.get(Person.class, personFromPost.getId());

        // set here explicitly what must/can be overwritten by the html form POST
        existingPerson.setName(personFromPost.getName());
        existingPerson.setEmail(personFromPost.getEmail());
        existingPerson.setDateModified(new Date());
        existingPerson.setUserModified(Utils.getCurrentUser());

        session.update(existingPerson);
    }

}

EDIT 1

There is in fact a Spring-way to solve this issue, using @SessionAttributes, see this anwer:

https://mcmap.net/q/540704/-spring-mvc-3-0-how-do-i-bind-to-a-persistent-object

I did not yet test it, but it looks promising.

EDIT 2

Eventually I tested it and it works as expected.

There is one thing however which can make you shoot in your foot:

If you open several tabs with the same form, the opening of the last tab overwrites the sessionAttribute of the others, and, on submit, can potentially corrupt your data. There is a solution in this blog post: http://marty-java-dev.blogspot.com/2010/09/spring-3-session-level-model-attributes.html

But at the end, if you never open multiple tabs for editing, you will not have a problem anyway.

Sensory answered 5/2, 2013 at 8:22 Comment(2)
This worked, Thank you! But I can't help but feel like there should be a cleaner way of doing it. It's going to be painful to have to define a function like this for every form. I added the mergeWithExistingAndUpdate method to my User model (see the edit on my question for the final code). I don't think I need all the session stuff, since I can use the EntityManager that Roo set up for me. I'm unfamiliar with the concept of a "service layer". Where would you put this UserService class? And how would you call the mergeWithExistingAndUpdate method from the controller action?Dispraise
I agree with you that it's not 100% clean :-) The "Service Layer" idea is nicely visible in this blog post: krams915.blogspot.com/2011/01/… The method mergeWithExistingAndUpdate is called from the Controller which get's the update-request by POST. HTHSensory
G
15

If you never want to update a particular attribute, you can mark it with updatable=false:

@Column(name="CREATED_ON", updatable=false)
private Date createdOn;

Once you load an entity and you modify it, as long as the current Session or EntityManager is open, Hibernate can track changes through the dirty checking mechanism. Then, during flush, an SQL UPDATE will be executed.

If you don't like that all columns are included in the UPDATE statement, you can use dynamic update:

@Entity
@DynamicUpdate
public class Product {
   //code omitted for brevity
}

Then, only the modified columns will be included in the UPDATE statement.

Geoid answered 11/10, 2016 at 7:14 Comment(0)
H
0

If your persistence provider is Hibernate, use the hibernate-specific annotation: @DynamicUpdate on the entity:

For updating, should this entity use dynamic sql generation where only changed columns get referenced in the prepared sql statement?

Note, for re-attachment of detached entities this is not possible without select-before-update being enabled.

Hormone answered 5/2, 2013 at 6:2 Comment(2)
Thanks for the suggestion. This doesn't quite solve my problem though. I was using Hibernate 3.6, but upgraded to 4.1 to get the @DynamicUpdate annotation. I added it to my entity, but it still overwrites in the database the model attributes not included in the form. It looks like the User entity's password attribute is set as null when it comes into the request handler (because it wasn't included in the form), so Hibernate thinks it has changed (from some value to null), and includes it in the UPDATE query.Dispraise
if if we not using Hibernate?Baun

© 2022 - 2024 — McMap. All rights reserved.