26

We are trying to implement a special partial update function in Spring 3.2. We are using Spring for the backend and have a simple Javascript frontend. I've not been able to find a straight-forward solution to our requirements, which is The update() function should take in any number of field:values and update the persistence model accordingly.

We have in-line editing for all of our fields, so that when the user edits a field and confirms, an id and the modified field get passed to the controller as json. The controller should be able to take in any number of fields from the client (1 to n) and update only those fields.

e.g., when a user with id==1 edits his displayName, the data posted to the server looks like this:

{"id":"1", "displayName":"jim"}

Currently, we have an incomplete solution in the UserController as outlined below:

@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@RequestBody User updateUser) {
    dbUser = userRepository.findOne(updateUser.getId());
    customObjectMerger(updateUser, dbUser);
    userRepository.saveAndFlush(updateUuser);
    ...
}

The code here works, but has some issues: The @RequestBody creates a new updateUser, fills in the id and the displayName. CustomObjectMerger merges this updateUser with the corresponding dbUser from the database, updating the only fields included in updateUser.

The problem is that Spring populates some fields in updateUser with default values and other auto-generated field values, which, upon merging, overwrites valid data that we have in dbUser. Explicitly declaring that it should ignore these fields is not an option, as we want our update to be able to set these fields as well.

I am looking into some way to have Spring automatically merge ONLY the information explicitly sent into the update() function into the dbUser (without resetting default/auto field values). Is there any simple way to do this?

Update: I've already considered the following option which does almost what I'm asking for, but not quite. The problem is that it takes update data in as @RequestParam and (AFAIK) doesn't do JSON strings:

//load the existing user into the model for injecting into the update function
@ModelAttribute("user")
public User addUser(@RequestParam(required=false) Long id){
    if (id != null) return userRepository.findOne(id);
    return null;
}
....
//method declaration for using @MethodAttribute to pre-populate the template object
@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@ModelAttribute("user") User updateUser){
....
}

I've considered re-writing my customObjectMerger() to work more appropriately with JSON, counting and having it take into consideration only the fields coming in from HttpServletRequest. but even having to use a customObjectMerger() in the first place feels hacky when spring provides almost exactly what I am looking, minus the lacking JSON functionality. If anyone knows of how to get Spring to do this, I'd greatly appreciate it!

1
  • @SamEsla - Did you ever find a better solution, in particular for the situation where there are nested objects? I have the same issue, and prior to reading this post, did something similar to you.. If you have time please see: stackoverflow.com/questions/16473727/… Commented May 12, 2013 at 8:10

8 Answers 8

27

I've just run into this same problem. My current solution looks like this. I haven't done much testing yet, but upon initial inspection it looks to be working fairly well.

@Autowired ObjectMapper objectMapper;
@Autowired UserRepository userRepository;

@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@PathVariable Long id, HttpServletRequest request) throws IOException
{
    User user = userRepository.findOne(id);
    User updatedUser = objectMapper.readerForUpdating(user).readValue(request.getReader());
    userRepository.saveAndFlush(updatedUser);
    return new ResponseEntity<>(updatedUser, HttpStatus.ACCEPTED);
}

The ObjectMapper is a bean of type org.codehaus.jackson.map.ObjectMapper.

Hope this helps someone,

Edit:

Have run into issues with child objects. If a child object receives a property to partially update it will create a fresh object, update that property, and set it. This erases all the other properties on that object. I'll update if I come across a clean solution.

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you Tyler, this seems like a handy solution. I still wonder whether there is a Spring function to allow direct/automatic binding of JSON from POST data to a persisted object.
Thank you for the solution. I thought that the only way to solve this situation is processing manually. Haven't ever known Jackson ObjectMapper before...
@Tyler - Did you ever find a better solution, in particular for the situation where there are nested objects? I have the same issue, and prior to reading this post, did something similar to the OP. If you have time please see: stackoverflow.com/questions/16473727/…
@user36123 - Take a look at this old bug for Jackson: jira.codehaus.org/browse/JACKSON-679 There is a comment showing some code that can do deep updating. Of course the API has changed since 2011, but we were able to fix it up and get it working.
@MrException - Thanks for pointing this out. That project is done and dusted now. But the details you provided, without actually testing the code snippet out, was exactly what I was looking for. Hope it may help others that read this post looking for the same solution. If you were able to fix it up to work with new api - would appreciate it if you would update my POST (stackoverflow.com/questions/16473727/…) with your solution. If it is working I'll credit your contribution as correct answer! Thanks.
|
4

We are using @ModelAttribute to achive what you want to do.

  • Create a method annotated with@modelattribute which loads a user based on a pathvariable throguh a repository.

  • create a method @Requestmapping with a param @modelattribute

The point here is that the @modelattribute method is the initializer for the model. Then spring merges the request with this model since we declare it in the @requestmapping method.

This gives you partial update functionality.

Some , or even alot? ;) would argue that this is bad practice anyway since we use our DAOs directly in the controller and do not do this merge in a dedicated service layer. But currently we did not ran into issues because of this aproach.

5 Comments

Thank you Martin, but I have already tried this. The problem with this method is that it seems to take in only @RequestParam for updating the fields. We want a way to duplicate this exact functionality that you mentioned, but working with JSON as the input. I'm sure that Spring has built-in functionality for this somewhere, but I haven't run into it yet.
This should work similar. True we use it currently with post data but i will try to do it with a json object. In the end spring is handling this very similar. Anything (requestparam, json props ) matching your model arributes should be merged. I will try to do a short example tomorrow since im on an ipad atm.
More than 3 weeks late for my answer.. But finally i had some time to check it out a little. Hoàng Long is right. There is no cleaner way of handling json requestbodys. Looks like we are missing a requestbody merger :) Could be a spring request?
@Martin - Did you ever approach Spring community with this request? I have a similar issue now. I would like to be able to make a post request using JSON request type, and have an existing Command object updated with the values that were altered as a result of the POST request. If interested, or have time, please see stackoverflow.com/questions/16473727/…
OK, I've opened a JIRA for this one: jira.springsource.org/browse/SPR-10552
3

I build an API that merge view objects with entities before call persiste or merge or update.

It's a first version but I think It's a start.

Just use the annotation UIAttribute in your POJO`S fields then use:

MergerProcessor.merge(pojoUi, pojoDb);

It works with native Attributes and Collection.

git: https://github.com/nfrpaiva/ui-merge

Comments

3

Following approach could be used.

For this scenario, PATCH method would be more appropriate since the entity will be partially updated.

In controller method, take the request body as string.

Convert that String to JSONObject. Then iterate over the keys and update matching variable with the incoming data.

import org.json.JSONObject;

@RequestMapping(value = "/{id}", method = RequestMethod.PATCH )
public ResponseEntity<?> updateUserPartially(@RequestBody String rawJson, @PathVariable long id){

    dbUser = userRepository.findOne(id);

    JSONObject json = new JSONObject(rawJson);

    Iterator<String> it = json.keySet().iterator();
    while(it.hasNext()){
        String key = it.next();
        switch(key){
            case "displayName":
                dbUser.setDisplayName(json.get(key));
                break;
            case "....":
                ....
        }
    }
    userRepository.save(dbUser);
    ...
}

Downside of this approach is, you have to manually validate the incoming values.

1 Comment

Hi there! Thanks for the response. Yeah, in hindsight PATCH seems like the natural solution, but I guess none of us considered it since it was just implemented a couple of months back as new Spring Framework functionality in version 3.2.
2

I've a customized and dirty solution employs java.lang.reflect package. My solution worked well for 3 years with no problem.

My method takes 2 arguments, objectFromRequest and objectFromDatabase both have the type Object.

The code simply does:

if(objectFromRequest.getMyValue() == null){
   objectFromDatabase.setMyValue(objectFromDatabase.getMyValue); //change nothing
} else {
   objectFromDatabase.setMyValue(objectFromRequest.getMyValue); //set the new value
}

A "null" value in a field from request means "don't change it!".

-1 value for a reference column which have name ending with "Id" means "Set it to null".

You can also add many custom modifications for your different scenarios.

public static void partialUpdateFields(Object objectFromRequest, Object objectFromDatabase) {
    try {
        Method[] methods = objectFromRequest.getClass().getDeclaredMethods();

        for (Method method : methods) {
            Object newValue = null;
            Object oldValue = null;
            Method setter = null;
            Class valueClass = null;
            String methodName = method.getName();
            if (methodName.startsWith("get") || methodName.startsWith("is")) {
                newValue = method.invoke(objectFromRequest, null);
                oldValue = method.invoke(objectFromDatabase, null);

                if (newValue != null) {
                    valueClass = newValue.getClass();
                } else if (oldValue != null) {
                    valueClass = oldValue.getClass();
                } else {
                    continue;
                }
                if (valueClass == Timestamp.class) {
                    valueClass = Date.class;
                }

                if (methodName.startsWith("get")) {
                    setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("get", "set"),
                            valueClass);
                } else {
                    setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("is", "set"),
                            valueClass);
                }

                if (newValue == null) {
                    newValue = oldValue;
                }

                if (methodName.endsWith("Id")
                        && (valueClass == Number.class || valueClass == Integer.class || valueClass == Long.class)
                        && newValue.equals(-1)) {
                    setter.invoke(objectFromDatabase, new Object[] { null });
                } else if (methodName.endsWith("Date") && valueClass == Date.class
                        && ((Date) newValue).getTime() == 0l) {
                    setter.invoke(objectFromDatabase, new Object[] { null });
                } 
                else {
                    setter.invoke(objectFromDatabase, newValue);
                }
            }

        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

In my DAO class, simcardToUpdate comes from http request:

simcardUpdated = (Simcard) session.get(Simcard.class, simcardToUpdate.getId());

MyUtil.partialUpdateFields(simcardToUpdate, simcardUpdated);

updatedEntities = Integer.parseInt(session.save(simcardUpdated).toString());

Comments

0

The main problem lies in your following code:

@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@RequestBody User updateUser) {
    dbUser = userRepository.findOne(updateUser.getId());
    customObjectMerger(updateUser, dbUser);
    userRepository.saveAndFlush(updateUuser);
    ...
}

In the above functions, you call some of your private functions & classes (userRepository, customObjectMerger, ...), but give no explanation how it works or how those functions look like. So I can only guess:

CustomObjectMerger merges this updateUser with the corresponding dbUser from the database, updating the only fields included in updateUser.

Here we don't know what happened in CustomObjectMerger (that's your function, and you don't show it). But from what you describe, I can make a guess: you copy all the properties from updateUser to your object at database. This is absolutely a wrong way, since when Spring map the object, it will fill all the data. And you only want to update some specific properties.

There are 2 options in your case:

1) Sending all the properties (including the unchanged properties) to the server. This may cost a little more bandwidth, but you still keep your way

2) You should set some special values as the default value for the User object (for example, id = -1, age = -1...). Then in customObjectMerger you just set the value that is not -1.

If you feel the 2 above solutions aren't satisfied, consider parsing the json request yourself, and don't bother with Spring object mapping mechanism. Sometimes it just confuse a lot.

4 Comments

Hi Hoang, thank you for your answer. @RequestBody turns my payload into a new instance of the User object. The innards of the private methods are not important to the question at hand. Please look at Martin Fey's answer and my update to see how @ModelAttribute works to serve the same functionality I am looking for, but works with RequestParams coming in the request. Using this method, Spring automatically binds the new information from the RequestParams to an an existing object it gets from the database. I've been scouring the Spring Docs for a way to do this with a JSON payload.
@SamEsla: I understand Fey's method and your question. In my opinion, Fey's way is smart, but it's not quite a good practice. I my self experience some problems when working with ModelAttribute earlier, especially, as he comment, that's working with DAO object in controller.
@SamEsla: Oh, I misread your question. I wrote RequestBody but somehow I think you mean ResponseBody. How bad my eyes are @_@ I have updated the answer to remove that irrelevant part.
Thanks Hoang, yes, the @RequestBody uses an ObjectMapper to bind incoming data from the request to a new instance of the specified object.
0

Partial updates can be solved by using @SessionAttributes functionality, which are made to do what you did yourself with the customObjectMerger.

Look at my answer here, especially the edits, to get you started:

https://stackoverflow.com/a/14702971/272180

Comments

0

I've done this with a java Map and some reflection magic:

public static Entidade setFieldsByMap(Map<String, Object> dados, Entidade entidade) {
        dados.entrySet().stream().
                filter(e -> e.getValue() != null).
                forEach(e -> {
                    try {
                        Method setter = entidade.getClass().
                                getMethod("set"+ Strings.capitalize(e.getKey()),
                                        Class.forName(e.getValue().getClass().getTypeName()));
                        setter.invoke(entidade, e.getValue());
                    } catch (Exception ex) { // a lot of exceptions
                        throw new WebServiceRuntimeException("ws.reflection.error", ex);
                    }
                });
        return entidade;
    }

And the entry point:

    @Transactional
    @PatchMapping("/{id}")
    public ResponseEntity<EntityOutput> partialUpdate(@PathVariable String entity,
            @PathVariable Long id, @RequestBody Map<String, Object> data) {
        // ...
        return new ResponseEntity<>(obj, HttpStatus.OK);
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.