Spring Partial Update Object Data Binding

后端 未结 8 1676
挽巷
挽巷 2020-12-02 08: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

相关标签:
8条回答
  • 2020-12-02 08:56

    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());
    
    0 讨论(0)
  • 2020-12-02 08:58

    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);
        }
    
    0 讨论(0)
  • 2020-12-02 09:00

    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.

    0 讨论(0)
  • 2020-12-02 09:02

    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

    0 讨论(0)
  • 2020-12-02 09:10

    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.

    0 讨论(0)
  • 2020-12-02 09:14

    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.

    0 讨论(0)
提交回复
热议问题