使用Spring Data Rest和MongoDB更新特定字段

huangapple go评论74阅读模式
英文:

Update Specific Fields with Spring Data Rest and MongoDB

问题

我正在使用Spring Data MongoDB和Spring Data Rest来创建一个REST API,允许在我的MongoDB数据库上进行GET、POST、PUT和DELETE操作,除了更新操作(PUT)之外,一切都运行正常。只有当我在请求主体中发送完整对象时,它才起作用。

例如,我有以下实体:

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

    ...
}

要更新lastName字段,我必须发送整个用户对象,包括密码!这显然是非常错误的。
如果我只发送要更新的字段,所有其他字段都会在我的数据库中设置为null。我甚至尝试在这些字段上添加@NotNull约束,现在除非我发送用户对象的所有字段,否则更新甚至不会发生。

我尝试在这里寻找解决方案,但我只找到了以下帖子,没有解决方案:https://stackoverflow.com/questions/38973231/how-to-update-particular-field-in-mongo-db-by-using-mongorepository-interface

有办法实现这个吗?

英文:

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: https://stackoverflow.com/questions/38973231/how-to-update-particular-field-in-mongo-db-by-using-mongorepository-interface

Is there a way to implement this ?

答案1

得分: 2

Spring Data Rest 使用 Spring Data 存储库来通过 REST 调用自动检索和操作持久数据(请查看 https://docs.spring.io/spring-data/rest/docs/current/reference/html/#reference)。

在使用 Spring Data MongoDB 时,您有 MongoOperations 接口,该接口用作 Rest 端点的存储库。
然而,MongoOperations 目前不支持特定字段的更新!

附注:如果他们像 Spring Data JPA 中的 @DynamicUpdate 一样添加了这个功能,那将是很棒的。

但这并不意味着不能做到,以下是我在遇到这个问题时采取的解决方法。

首先,让我解释一下我们要做的事情:

  1. 我们将创建一个控制器,覆盖所有的 PUT 操作,以便我们可以实现自己的更新方法。
  2. 在更新方法内部,我们将使用具有更新特定字段能力的 MongoTemplate。

注:我们不希望为应用程序中的每个模型重新执行这些步骤,因此我们将动态检索要更新的模型。为了实现这一点,我们将创建一个实用类。[这是可选的]

让我们首先将 org.reflections API 添加到项目依赖中,它允许我们获取所有带有特定注释(在我们的情况下是 @Document)的类:

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

然后创建一个名为 UpdateUtility 的新类,并添加以下方法,同时用您自己的包含实体的包替换 MODEL_PACKAGE 属性:

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); // 获取在指定包中用 @Document 注释的所有类

            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 exist!");
        }
    }
}

使用这个实用类,我们可以根据类型检索要更新的模型类。
例如:UpdateUtility.getClassFromType() 将返回 User.class

现在,让我们创建我们的控制器:

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); // 从请求中的类型获取域类
            Query query = new Query(Criteria.where("id").is(id)); // 使用给定的 ID 更新文档
            Update update = new Update();

            // 遍历发送的字段并将其添加到更新对象中
            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); // 执行更新
            return mongoTemplate.findById(id, classType); // 返回更新后的文档
        } catch (Exception e) {
            // 处理您的异常
        }
    }
}

现在,我们能够在不更改调用的情况下更新指定的字段。
所以在您的情况下,调用将是:

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

附注:您可以通过添加在嵌套对象中更新特定字段的可能性来改进它…

英文:

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.

N.B. 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):

&lt;dependency&gt;
    &lt;groupId&gt;org.reflections&lt;/groupId&gt;
    &lt;artifactId&gt;reflections&lt;/artifactId&gt;
    &lt;version&gt;0.9.12&lt;/version&gt;
&lt;/dependency&gt;

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 = &quot;com.mycompany.myproject.models&quot;;
    private static boolean initialized =  false;
    private static HashMap&lt;String, Class&gt; classContext = new HashMap&lt;&gt;();

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

            for(Class&lt;?&gt; 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(&quot;Type &quot; + type + &quot; does not exists !&quot;);
        }
    }
}

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

Now let's create our controller:

public class UpdateController {

    @Autowired
    private MongoTemplate mongoTemplate;

    @PutMapping(&quot;/{type}/{id}&quot;)
    public Object update(@RequestBody HashMap&lt;String, Object&gt; fields,
                                  @PathVariable(name = &quot;type&quot;) String type,
                                  @PathVariable(name = &quot;id&quot;) String id) {
        try {
            Class classType = UpdatorUtility.getClassFromType(type); // Get the domain class from the type in the request
            Query query = new Query(Criteria.where(&quot;id&quot;).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: &quot;My new last name&quot; }

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

huangapple
  • 本文由 发表于 2020年5月4日 20:51:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/61592630.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定