跳到主要内容

保存、更新和删除文档

DeepSeek V3 中英对照 Saving, Updating, and Removing Documents

MongoTemplate / ReactiveMongoTemplate 允许你保存、更新和删除你的领域对象,并将这些对象映射到存储在 MongoDB 中的文档。命令式和响应式 API 的签名基本相同,只是在返回类型上有所不同。同步 API 使用 void、单个 ObjectList,而响应式 API 则由 Mono<Void>Mono<Object>Flux 组成。

考虑以下类:

public class Person {

private String id;
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
java

给定前面示例中的 Person 类,你可以保存、更新和删除对象,如下例所示:

public class MongoApplication {

private static final Log log = LogFactory.getLog(MongoApplication.class);

public static void main(String[] args) {

MongoOperations template = new MongoTemplate(new SimpleMongoClientDbFactory(MongoClients.create(), "database"));

Person p = new Person("Joe", 34);

// Insert 用于最初将对象存储到数据库中。
template.insert(p);
log.info("Insert: " + p);

// 查找
p = template.findById(p.getId(), Person.class);
log.info("Found: " + p);

// 更新
template.updateFirst(query(where("name").is("Joe")), update("age", 35), Person.class);
p = template.findOne(query(where("name").is("Joe")), Person.class);
log.info("Updated: " + p);

// 删除
template.remove(p);

// 检查删除是否成功
List<Person> people = template.findAll(Person.class);
log.info("Number of people = : " + people.size());

template.dropCollection(Person.class);
}
}
java

前面的示例将产生以下日志输出(包括来自 MongoTemplate 的调试消息):

DEBUG apping.MongoPersistentEntityIndexCreator:  80 - 正在分析类 org.spring.example.Person 的索引信息。
DEBUG work.data.mongodb.core.MongoTemplate: 632 - 在集合 person 中插入包含字段 [_class, age, name] 的文档
INFO org.spring.example.MongoApp: 30 - Insert: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate:1246 - 在数据库集合 database.person 中使用查询 { "_id" : { "$oid" : "4ddc6e784ce5b1eba3ceaf5c"}} 进行 findOne 操作
INFO org.spring.example.MongoApp: 34 - Found: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate: 778 - 使用查询 { "name" : "Joe"} 和更新 { "$set" : { "age" : 35}} 在集合 person 中调用 update
DEBUG work.data.mongodb.core.MongoTemplate:1246 - 在数据库集合 database.person 中使用查询 { "name" : "Joe"} 进行 findOne 操作
INFO org.spring.example.MongoApp: 39 - Updated: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=35]
DEBUG work.data.mongodb.core.MongoTemplate: 823 - 使用查询 { "id" : "4ddc6e784ce5b1eba3ceaf5c"} 在集合 person 中调用 remove
INFO org.spring.example.MongoApp: 46 - Number of people = : 0
DEBUG work.data.mongodb.core.MongoTemplate: 376 - 删除集合 [database.person]
none

MongoConverter 通过识别(按照约定)Id 属性名称,导致在数据库中存储的 StringObjectId 之间进行隐式转换。

前面的示例旨在展示 MongoTemplate / ReactiveMongoTemplate 上的保存、更新和删除操作的使用,而不是展示复杂的映射功能。前面示例中使用的查询语法在“查询文档”部分有更详细的解释。

important

MongoDB 要求所有文档都必须有一个 _id 字段。有关此字段特殊处理的详细信息,请参阅 ID 处理 部分。

important

MongoDB 集合可以包含表示多种类型实例的文档。有关详细信息,请参阅 类型映射

插入 / 保存

MongoTemplate 上有几种方便的方法用于保存和插入对象。为了更细致地控制转换过程,你可以向 MappingMongoConverter 注册 Spring 转换器,例如 Converter<Person, Document>Converter<Document, Person>

备注

插入(insert)和保存(save)操作之间的区别在于,如果对象尚不存在,保存操作会执行插入操作。

使用保存操作的简单情况是保存一个 POJO。在这种情况下,集合名称由类的名称(非完全限定)决定。您也可以使用特定的集合名称调用保存操作。您可以使用映射元数据来覆盖存储对象的集合。

在插入或保存时,如果 Id 属性未设置,系统会假设其值将由数据库自动生成。因此,为了成功自动生成 ObjectId,类中的 Id 属性或字段的类型必须为 StringObjectIdBigInteger

以下示例展示了如何保存文档并检索其内容:

import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Criteria.query;

//...

template.insert(new Person("Bob", 33));

Person person = template.query(Person.class)
.matching(query(where("age").is(33)))
.oneValue();
java

以下是可用的插入和保存操作:

  • void 保存 (Object 要保存的对象): 将对象保存到默认集合中。

  • void 保存 (Object 要保存的对象, String 集合名称): 将对象保存到指定的集合中。

类似的插入操作集也同样可用:

  • void insert (Object objectToSave):将对象插入到默认集合中。

  • void insert (Object objectToSave, String collectionName):将对象插入到指定的集合中。

如何处理映射层中的 _id 字段

MongoDB 要求所有文档都必须有一个 _id 字段。如果你没有提供该字段,驱动程序会自动分配一个带有生成值的 ObjectId,而不会考虑你的领域模型,因为服务器并不知道你的标识符类型。当你使用 MappingMongoConverter 时,某些规则决定了如何将 Java 类中的属性映射到这个 _id 字段:

  1. 使用 @Idorg.springframework.data.annotation.Id)注解的属性或字段将映射到 _id 字段。

  2. 没有注解但名为 id 的属性或字段也将映射到 _id 字段。

以下概述了在使用 MappingMongoConverterMongoTemplate 的默认转换器)时,映射到 _id 文档字段的属性所进行的类型转换(如果有的话)。

  1. 如果可能,通过使用 Spring 的 Converter<String, ObjectId>,Java 类中声明为 Stringid 属性或字段会被转换为 ObjectId 并存储。有效的转换规则由 MongoDB Java 驱动程序处理。如果无法将其转换为 ObjectId,则该值将以字符串形式存储在数据库中。

  2. Java 类中声明为 BigIntegerid 属性或字段,通过使用 Spring 的 Converter<BigInteger, ObjectId>,会被转换为 ObjectId 并存储。

如果在之前的规则集中没有指定 Java 类中的任何字段或属性,驱动程序会生成一个隐式的 _id 文件,但不会将其映射到 Java 类的属性或字段。

在查询和更新时,MongoTemplate 会使用与前面保存文档规则对应的转换器,以确保查询中使用的字段名和类型与域类中的内容匹配。

在某些环境中,需要对 Id 值进行自定义映射,例如存储在 MongoDB 中但未经过 Spring Data 映射层处理的数据。文档可能包含 _id 值,这些值可以表示为 ObjectIdString。将文档从存储中读取回域类型时,一切正常。然而,由于隐式的 ObjectId 转换,通过 id 查询文档可能会变得繁琐。因此,无法通过这种方式检索文档。针对这种情况,@MongoId 提供了对实际 id 映射尝试的更多控制。

示例 1. @MongoId 映射

public class PlainStringId {
@MongoId String id; 1
}

public class PlainObjectId {
@MongoId ObjectId id; 2
}

public class StringToObjectId {
@MongoId(FieldType.OBJECT_ID) String id; 3
}
java
  • id 被视为 String,无需进一步转换。

  • id 被视为 ObjectId

  • 如果给定的 String 是有效的 ObjectId 十六进制,则 id 被视为 ObjectId,否则视为 String。对应于 @Id 的使用。

我的文档保存到哪个集合中?

有两种方式来管理用于文档的集合名称。默认使用的集合名称是将类名改为以小写字母开头。因此,一个 com.test.Person 类将被存储在 person 集合中。你可以通过使用 @Document 注解提供一个不同的集合名称来自定义此行为。你也可以通过在你选择的 MongoTemplate 方法调用中提供你自己的集合名称作为最后一个参数来覆盖集合名称。

插入或保存单个对象

MongoDB 驱动程序支持在一次操作中插入多个文档。MongoOperations 接口中的以下方法支持此功能:

  • insert:插入一个对象。如果已经存在具有相同 id 的文档,则会生成一个错误。

  • insertAll:将一组对象的 Collection 作为第一个参数。此方法会检查每个对象,并根据前面指定的规则将其插入到相应的集合中。

  • save:保存对象,覆盖可能具有相同 id 的任何对象。

批量插入多个对象

MongoDB 驱动程序支持在一次操作中插入一组文档。MongoOperations 接口中的以下方法通过 insert 或专门的 BulkOperations 接口来支持这一功能。

Collection<Person> inserted = template.insert(List.of(...), Person.class);
java
BulkWriteResult result = template.bulkOps(BulkMode.ORDERED, Person.class)
.insert(List.of(...))
.execute();
java
备注

批处理和批量操作的服务器性能是相同的。然而,批量操作不会发布生命周期事件

important

在调用 insert 之前未设置的任何 @Version 属性将自动初始化为 1(对于简单类型如 int)或 0(对于包装类型如 Integer)。
详细信息请参见乐观锁部分。

更新

对于更新操作,你可以使用 MongoOperation.updateFirst 来更新找到的第一个文档,或者使用 MongoOperation.updateMulti 方法或流畅 API 中的 all 来更新所有匹配查询的文档。以下示例展示了如何使用 $inc 运算符对所有 SAVINGS 账户进行更新,我们将一次性增加 $50.00 的奖金到余额中:

import static org.springframework.data.mongodb.core.query.Criteria.where;
import org.springframework.data.mongodb.core.query.Update;

// ...

UpdateResult result = template.update(Account.class)
.matching(where("accounts.accountType").is(Type.SAVINGS))
.apply(new Update().inc("accounts.$.balance", 50.00))
.all();
java

除了前面讨论的 Query 之外,我们还通过使用 Update 对象来提供更新定义。Update 类具有与 MongoDB 可用的更新修饰符相匹配的方法。大多数方法会返回 Update 对象,以便为 API 提供流畅的编程风格。

important

如果 Update 中没有包含 @Version 属性,它将自动递增。更多信息请参见乐观锁部分。

文档更新运行方法

  • updateFirst:根据查询文档条件更新第一个匹配的文档。

  • updateMulti:根据查询文档条件更新所有匹配的对象。

注意

updateFirst 不支持排序。请使用 findAndModify 来应用 Sort

备注

可以通过 Query.withHint(…​) 提供更新操作的索引提示。

Update 类中的方法

你可以对 Update 类使用一些“语法糖”,因为它的方法设计为可以链式调用。此外,你可以通过使用 public static Update update(String key, Object value) 并结合静态导入来快速创建一个新的 Update 实例。

Update 类包含以下方法:

  • Update addToSet (String key, Object value) 使用 $addToSet 更新修饰符进行更新

  • Update currentDate (String key) 使用 $currentDate 更新修饰符进行更新

  • Update currentTimestamp (String key) 使用带有 $type timestamp$currentDate 更新修饰符进行更新

  • Update inc (String key, Number inc) 使用 $inc 更新修饰符进行更新

  • Update max (String key, Object max) 使用 $max 更新修饰符进行更新

  • Update min (String key, Object min) 使用 $min 更新修饰符进行更新

  • Update multiply (String key, Number multiplier) 使用 $mul 更新修饰符进行更新

  • Update pop (String key, Update.Position pos) 使用 $pop 更新修饰符进行更新

  • Update pull (String key, Object value) 使用 $pull 更新修饰符进行更新

  • Update pullAll (String key, Object[] values) 使用 $pullAll 更新修饰符进行更新

  • Update push (String key, Object value) 使用 $push 更新修饰符进行更新

  • Update pushAll (String key, Object[] values) 使用 $pushAll 更新修饰符进行更新

  • Update rename (String oldName, String newName) 使用 $rename 更新修饰符进行更新

  • Update set (String key, Object value) 使用 $set 更新修饰符进行更新

  • Update setOnInsert (String key, Object value) 使用 $setOnInsert 更新修饰符进行更新

  • Update unset (String key) 使用 $unset 更新修饰符进行更新

一些更新修饰符,例如 $push$addToSet,允许嵌套其他操作符。

// { $push : { "category" : { "$each" : [ "spring" , "data" ] } } }
new Update().push("category").each("spring", "data")

// { $push : { "key" : { "$position" : 0 , "$each" : [ "Arya" , "Arry" , "Weasel" ] } } }
new Update().push("key").atPosition(Position.FIRST).each(Arrays.asList("Arya", "Arry", "Weasel"));

// { $push : { "key" : { "$slice" : 5 , "$each" : [ "Arya" , "Arry" , "Weasel" ] } } }
new Update().push("key").slice(5).each(Arrays.asList("Arya", "Arry", "Weasel"));

// { $addToSet : { "values" : { "$each" : [ "spring" , "data" , "mongodb" ] } } }
new Update().addToSet("values").each("spring", "data", "mongodb");
java

聚合管道更新

MongoOperationsReactiveMongoOperations 暴露的更新方法也接受通过 AggregationUpdate 传递的 聚合管道。使用 AggregationUpdate 可以在更新操作中利用 MongoDB 4.2 的聚合功能。在更新中使用聚合可以通过一个操作表达多个阶段和多个条件,从而更新一个或多个字段。

更新可以包含以下阶段:

  • AggregationUpdate.set(…​).toValue(…​)$set : { …​ }

  • AggregationUpdate.unset(…​)$unset : [ …​ ]

  • AggregationUpdate.replaceWith(…​)$replaceWith : { …​ }

示例 2. 更新聚合

AggregationUpdate update = Aggregation.newUpdate()
.set("average").toValue(ArithmeticOperators.valueOf("tests").avg()) 1
.set("grade").toValue(ConditionalOperators.switchCases( 2
when(valueOf("average").greaterThanEqualToValue(90)).then("A"),
when(valueOf("average").greaterThanEqualToValue(80)).then("B"),
when(valueOf("average").greaterThanEqualToValue(70)).then("C"),
when(valueOf("average").greaterThanEqualToValue(60)).then("D"))
.defaultTo("F")
);

template.update(Student.class) 3
.apply(update)
.all(); 4
java
db.students.update(                                                         3
{ },
[
{ $set: { average : { $avg: "$tests" } } }, 1
{ $set: { grade: { $switch: { 2
branches: [
{ case: { $gte: [ "$average", 90 ] }, then: "A" },
{ case: { $gte: [ "$average", 80 ] }, then: "B" },
{ case: { $gte: [ "$average", 70 ] }, then: "C" },
{ case: { $gte: [ "$average", 60 ] }, then: "D" }
],
default: "F"
} } } }
],
{ multi: true } 4
)
javascript
  • 第一个 $set 阶段根据 tests 字段的平均值计算一个新字段 average

  • 第二个 $set 阶段根据第一个聚合阶段计算的 average 字段计算一个新字段 grade

  • 该管道在 students 集合上运行,并使用 Student 作为聚合字段映射。

  • 将更新应用于集合中所有匹配的文档。

Upsert

与执行 updateFirst 操作相关的是,你还可以执行 upsert 操作,如果没有找到与查询匹配的文档,它将执行插入操作。插入的文档是查询文档和更新文档的组合。以下示例展示了如何使用 upsert 方法:

UpdateResult result = template.update(Person.class)
.matching(query(where("ssn").is(1111).and("firstName").is("Joe").and("Fraizer").is("Update"))
.apply(update("address", addr))
.upsert();
java
注意

upsert 不支持排序。请使用 findAndModify 来应用 Sort

important

如果不包含在 Update 中,@Version 属性将自动初始化。更多信息请参见 乐观锁 部分。

替换集合中的文档

通过 MongoTemplate 提供的各种 replace 方法可以覆盖第一个匹配的文档。如果未找到匹配项,可以通过提供配置了 ReplaceOptions 的选项来执行 upsert(如前一部分所述)。

Person tom = template.insert(new Person("Motte", 21)); 1
Query query = Query.query(Criteria.where("firstName").is(tom.getFirstName())); 2
tom.setFirstname("Tom"); 3
template.replace(query, tom, ReplaceOptions.none()); 4
java
  • 插入一个新文档。

  • 用于识别要替换的单个文档的查询。

  • 设置替换文档,该文档必须包含与现有文档相同的 _id 或者不包含 _id

  • 运行替换操作。.使用 upsert 替换一个文档

Person tom = new Person("id-123", "Tom", 21) // <1>
Query query = Query.query(Criteria.where("firstName").is(tom.getFirstName()));
template.replace(query, tom, ReplaceOptions.replaceOptions().upsert()); // <2>
  • _id 值需要在 upsert 操作中存在,否则 MongoDB 会创建一个新的可能不兼容域类型的 ObjectId。由于 MongoDB 不了解你的域类型,任何 @Field(targetType) 提示都不会被考虑,生成的 ObjectId 可能与你的域模型不兼容。

  • 使用 upsert 在没有找到匹配项时插入新文档

注意

无法通过替换操作更改现有文档的 _id。在 upsert 操作中,MongoDB 使用两种方式来确定条目的新 ID:* 在查询中使用 _id,例如 {"_id" : 1234 } * _id 存在于替换文档中。如果未通过任何一种方式提供 _id,MongoDB 将为文档创建一个新的 ObjectId。如果使用的域类型 id 属性具有不同的类型(例如 Long),这可能会导致映射和数据查找功能异常。

查找与修改

MongoCollection 上的 findAndModify(…) 方法可以更新文档,并在单个操作中返回旧的或新更新的文档。MongoTemplate 提供了四个 findAndModify 重载方法,这些方法接受 QueryUpdate 类,并将 Document 转换为你的 POJO:

<T> T findAndModify(Query query, Update update, Class<T> entityClass);
<T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass, String collectionName);
java
<T> T findAndModify(Query query, Update update, Class<T> entityClass);

<T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);

<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);

<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass, String collectionName);
java

以下示例将几个 Person 对象插入到容器中,并执行 findAndUpdate 操作:

template.insert(new Person("Tom", 21));
template.insert(new Person("Dick", 22));
template.insert(new Person("Harry", 23));

Query query = new Query(Criteria.where("firstName").is("Harry"));
Update update = new Update().inc("age", 1);

Person oldValue = template.update(Person.class)
.matching(query)
.apply(update)
.findAndModifyValue(); // oldValue.age == 23

Person newValue = template.query(Person.class)
.matching(query)
.findOneValue(); // newValye.age == 24

Person newestValue = template.update(Person.class)
.matching(query)
.apply(update)
.withOptions(FindAndModifyOptions.options().returnNew(true)) // Now return the newly updated document when updating
.findAndModifyValue(); // newestValue.age == 25
java

FindAndModifyOptions 方法允许你设置 returnNewupsertremove 等选项。以下是从之前的代码片段扩展而来的示例:

Person upserted = template.update(Person.class)
.matching(new Query(Criteria.where("firstName").is("Mary")))
.apply(update)
.withOptions(FindAndModifyOptions.options().upsert(true).returnNew(true))
.findAndModifyValue()
java
important

如果 Update 中未包含 @Version 属性,它将自动递增。更多信息请参见 乐观锁 部分。

查找和替换

最直接的替换整个 Document 的方法是通过其 id 使用 save 方法。然而,这并不总是可行的。findAndReplace 提供了一种替代方案,允许通过简单的查询来识别要替换的文档。

示例 3. 查找和替换文档

Optional<User> result = template.update(Person.class)      1
.matching(query(where("firstame").is("Tom"))) 2
.replaceWith(new Person("Dick"))
.withOptions(FindAndReplaceOptions.options().upsert()) 3
.as(User.class) 4
.findAndReplace(); 5
java
  • 使用带有给定域类型的流畅更新 API 来映射查询并派生集合名称,或者直接使用 MongoOperations#findAndReplace

  • 针对给定域类型映射的实际匹配查询。通过查询提供 sortfieldscollation 设置。

  • 额外的可选钩子,用于提供除默认值之外的其他选项,如 upsert

  • 用于映射操作结果的可选投影类型。如果未提供,则使用初始域类型。

  • 触发实际处理。使用 findAndReplaceValue 来获取可为空的结果,而不是 Optional

important

请注意,替换的文档本身不能包含 id,因为现有 Documentid 将由存储本身转移到替换文档中。此外,请记住,findAndReplace 只会根据可能给定的排序顺序替换第一个匹配查询条件的文档。

删除

你可以使用以下五种重载方法之一从数据库中移除对象:

template.remove(tywin, "GOT");                                              1

template.remove(query(where("lastname").is("lannister")), "GOT"); 2

template.remove(new Query().limit(3), "GOT"); 3

template.findAllAndRemove(query(where("lastname").is("lannister"), "GOT"); 4

template.findAllAndRemove(new Query().limit(3), "GOT"); 5
java
  • 从关联的集合中移除由其 _id 指定的单个实体。

  • GOT 集合中移除所有符合查询条件的文档。

  • 移除 GOT 集合中的前三个文档。与 <2> 不同,要移除的文档由其 _id 标识,先运行给定的查询,应用 sortlimitskip 选项,然后在单独的步骤中一次性移除所有文档。

  • GOT 集合中移除所有符合查询条件的文档。与 <3> 不同,文档不会批量删除,而是逐个删除。

  • 移除 GOT 集合中的前三个文档。与 <3> 不同,文档不会批量删除,而是逐个删除。

乐观锁

@Version 注解在 MongoDB 的上下文中提供了类似于 JPA 的语法,并确保更新仅应用于具有匹配版本的文档。因此,版本属性的实际值会被添加到更新查询中,以便在另一个操作在此期间修改了文档时,更新不会产生任何效果。在这种情况下,会抛出 OptimisticLockingFailureException 异常。以下示例展示了这些特性:

@Document
class Person {

@Id String id;
String firstname;
String lastname;
@Version Long version;
}

Person daenerys = template.insert(new Person("Daenerys")); 1

Person tmp = template.findOne(query(where("id").is(daenerys.getId())), Person.class); 2

daenerys.setLastname("Targaryen");
template.save(daenerys); 3

template.save(tmp); // throws OptimisticLockingFailureException // <4>
java
  • 最初插入文档。version 被设置为 0

  • 加载刚刚插入的文档。version 仍然是 0

  • 使用 version = 0 更新文档。设置 lastname 并将 version 增加至 1

  • 尝试更新之前加载的文档,该文档的 version 仍为 0。由于当前 version1,操作将失败并抛出 OptimisticLockingFailureException

只有对 MongoTemplate 进行的某些 CRUD 操作才会考虑并修改版本属性。详细信息请参阅 MongoOperations 的 Java 文档。

important

乐观锁(Optimistic Locking)需要将 WriteConcern 设置为 ACKNOWLEDGED。否则,OptimisticLockingFailureException 可能会被静默吞掉。

备注

从 2.2 版本开始,MongoOperations 在从数据库中删除实体时也会包含 @Version 属性。如果要在不进行版本检查的情况下删除 Document,请使用 MongoOperations#remove(Query,…​) 而不是 MongoOperations#remove(Object)

备注

从 2.2 版本开始,在删除版本化实体时,仓库会检查已确认删除的结果。如果无法通过 CrudRepository.delete(Object) 删除版本化实体,则会抛出 OptimisticLockingFailureException。在这种情况下,版本已被更改或对象已被删除。使用 CrudRepository.deleteById(ID) 可以绕过乐观锁定功能,无论对象的版本如何,都可以删除对象。