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!

Sam
  • 900
  • 3
  • 10
  • 16
  • @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: http://stackoverflow.com/questions/16473727/spring-3-ajax-post-request-with-requestbody-and-modelattribute-and-sessionatt – arcseldon May 12 '13 at 08:10

8 Answers8

26

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.

Tyler Eastman
  • 366
  • 2
  • 5
  • 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. – Sam Feb 28 '13 at 22:43
  • 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... – Hoàng Long Mar 01 '13 at 02:58
  • @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: http://stackoverflow.com/questions/16473727/spring-3-ajax-post-request-with-requestbody-and-modelattribute-and-sessionatt – arcseldon May 12 '13 at 08:09
  • @user36123 - Take a look at this old bug for Jackson: http://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 Sep 17 '13 at 22:49
  • @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 (http://stackoverflow.com/questions/16473727/spring-3-ajax-post-request-with-requestbody-and-modelattribute-and-sessionatt) with your solution. If it is working I'll credit your contribution as correct answer! Thanks. – arcseldon Sep 21 '13 at 04:32
  • @arcseldon and tayler, you can handle nested object merging by configuring the objectmapper with objectMapper.setDefaultMergeable(Boolean.TRUE) or if you prefer to configure the merge at field level you can set the JsonMerge annotation. This will work for objects and also for arrays when new elements are added. however, if your change involves removing of elements, the merge wont work properly. – abdel Mar 13 '19 at 10:26
  • @abdel - only 6 years later.. :) – arcseldon Mar 14 '19 at 10:00
  • @arcseldon lool, yes. but it is still very relevant question. just came across such merge issue coded, recently, by very experienced consultants. hope the answer is helpful to someone. – abdel Apr 05 '19 at 10:27
  • Thanks for sharing @abdel, appreciated. – arcseldon Apr 05 '19 at 11:23
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.

Martin Frey
  • 10,025
  • 4
  • 25
  • 30
  • 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. – Sam Feb 28 '13 at 17:42
  • 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. – Martin Frey Mar 01 '13 at 23:51
  • 1
    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 Frey Mar 24 '13 at 08:31
  • @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 http://stackoverflow.com/questions/16473727/spring-3-ajax-post-request-with-requestbody-and-modelattribute-and-sessionatt – arcseldon May 12 '13 at 08:14
  • OK, I've opened a JIRA for this one: https://jira.springsource.org/browse/SPR-10552 – arcseldon May 12 '13 at 09:19
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

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.

bafoly
  • 31
  • 1
  • 2
  • 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. – Sam Sep 16 '16 at 20:09
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());
Ismail Yavuz
  • 6,727
  • 6
  • 29
  • 50
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.

Hoàng Long
  • 10,746
  • 20
  • 75
  • 124
  • 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. – Sam Feb 28 '13 at 18:24
  • @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. – Hoàng Long Mar 01 '13 at 02:52
  • @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. – Hoàng Long Mar 01 '13 at 02:53
  • Thanks Hoang, yes, the @RequestBody uses an ObjectMapper to bind incoming data from the request to a new instance of the specified object. – Sam Mar 01 '13 at 18:17
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

Community
  • 1
  • 1
yglodt
  • 13,807
  • 14
  • 91
  • 127
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);
    }