简体   繁体   中英

Update Specific Fields with Spring Data Rest and MongoDB

I'm using Spring Data MongoDB and Spring Data Rest to create a REST API which allows GET, POST, PUT and DELETE operations on my MongoDB database and it's all working fine except for the update operations (PUT). It only works if I send the full object in the request body.

For example I have the following entity:

@Document
public class User {
    @Id
    private String id;
    private String email;
    private String lastName;
    private String firstName;
    private String password;

    ...
}

To update the lastName field, I have to send all of the user object, including the password ! which is obviously very wrong . If I only send the field to update, all the others are set to null in my database. I even tried to add a @NotNull constraints on those fields and now the update won't even happens unless I send all of the user object's fields.

I tried searching for a solution here but I only found the following post but with no solution: How to update particular field in mongo db by using MongoRepository Interface?

Is there a way to implement this?

Spring Data Rest uses Spring Data repositories to automatically retrieve and manipulate persistent data using Rest calls (check out https://docs.spring.io/spring-data/rest/docs/current/reference/html/#reference ).

When using Spring Data MongoDB, you have the MongoOperations interface which is used as a repository for your Rest endpoints. However MongoOperations currently does not supports specific fields updates !

PS: It will be awesome if they add this feature like @DynamicUpdate in Spring Data JPA

But this doesn't mean it can be done, here's the workaround I did when I had this issue.

Firstly let me explain what we're going to do:

  1. We will create a controller which will override all the PUT operations so that we can implement our own update method.
  2. Inside that update method, we will use MongoTemplate which do have the ability to update specific fields.

NB We don't want to re-do these steps for each model in our application, so we will retrieve which model to update dynamically. In order to do that we will create a utility class. [This is optional]

Let's start by adding the org.reflections api to our project dependency which allows us to get all the classes which have a specific annotation ( @Document in our case):

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.12</version>
</dependency>

Then create a new class, called UpdateUtility and add the following methods and also replace the MODEL_PACKAGE attribute with your own package containing your entities:

public class UpdateUtility {

    private static final String MODEL_PACKAGE = "com.mycompany.myproject.models";
    private static boolean initialized =  false;
    private static HashMap<String, Class> classContext = new HashMap<>();

    private static void init() {
        if(!initialized) {
            Reflections reflections = new Reflections(MODEL_PACKAGE);
            Set<Class<?>> classes = reflections.getTypesAnnotatedWith(Document.class); // Get all the classes annotated with @Document in the specified package

            for(Class<?> model : classes) {
                classContext.put(model.getSimpleName().toLowerCase(), model);
            }

            initialized = true;
        }
    }

    public static Class getClassFromType(String type) throws Exception{
        init();
        if(classContext.containsKey(type)) {
            return classContext.get(type);
        }
        else {
            throw new Exception("Type " + type + " does not exists !");
        }
    }
}

Using this utility class we can retreive the model class to update from it's type. Eg: UpdateUtility.getClassFromType() will returns User.class

Now let's create our controller:

public class UpdateController {

    @Autowired
    private MongoTemplate mongoTemplate;

    @PutMapping("/{type}/{id}")
    public Object update(@RequestBody HashMap<String, Object> fields,
                                  @PathVariable(name = "type") String type,
                                  @PathVariable(name = "id") String id) {
        try {
            Class classType = UpdatorUtility.getClassFromType(type); // Get the domain class from the type in the request
            Query query = new Query(Criteria.where("id").is(id)); // Update the document with the given ID
            Update update = new Update();

            // Iterate over the send fields and add them to the update object
            Iterator iterator = fields.entrySet().iterator();
            while(iterator.hasNext()) {
                HashMap.Entry entry = (HashMap.Entry) iterator.next();
                String key = (String) entry.getKey();
                Object value = entry.getValue();
                update.set(key, value);
            }

            mongoTemplate.updateFirst(query, update, classType); // Do the update
            return mongoTemplate.findById(id, classType); // Return the updated document
        } catch (Exception e) {
            // Handle your exception
        }
    }
}

Now we're able to update the specified fields without changing the calls. So in your case, the call would be:

PUT http://MY-DOMAIN/user/MY-USER-ID { lastName: "My new last name" }

PS: You can improve it by adding the possibility to update specific field in a nested objects...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM