保存、更新与移除文档
MongoTemplate / ReactiveMongoTemplate 让您可以保存、更新和删除您的领域对象,并将这些对象映射到存储在 MongoDB 中的文档。命令式 API 与响应式 API 的方法签名基本相同,仅返回类型不同。同步 API 使用 void、单个 Object 和 List,而对应的响应式 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 + "]";
}
}
给定前述示例中的 Person 类,您可以保存、更新和删除该对象,如下例所示:
- Imperative
- Reactive
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 is used to initially store the object into the database.
template.insert(p);
log.info("Insert: " + p);
// Find
p = template.findById(p.getId(), Person.class);
log.info("Found: " + p);
// Update
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);
// Delete
template.remove(p);
// Check that deletion worked
List<Person> people = template.findAll(Person.class);
log.info("Number of people = : " + people.size());
template.dropCollection(Person.class);
}
}
The preceding example would produce the following log output (including debug messages from MongoTemplate):
DEBUG apping.MongoPersistentEntityIndexCreator: 80 - Analyzing class class org.spring.example.Person for index information.
DEBUG work.data.mongodb.core.MongoTemplate: 632 - insert Document containing fields: [_class, age, name] in collection: person
INFO org.spring.example.MongoApp: 30 - Insert: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate:1246 - findOne using query: { "_id" : { "$oid" : "4ddc6e784ce5b1eba3ceaf5c"}} in db.collection: database.person
INFO org.spring.example.MongoApp: 34 - Found: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate: 778 - calling update using query: { "name" : "Joe"} and update: { "$set" : { "age" : 35}} in collection: person
DEBUG work.data.mongodb.core.MongoTemplate:1246 - findOne using query: { "name" : "Joe"} in db.collection: database.person
INFO org.spring.example.MongoApp: 39 - Updated: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=35]
DEBUG work.data.mongodb.core.MongoTemplate: 823 - remove using query: { "id" : "4ddc6e784ce5b1eba3ceaf5c"} in collection: person
INFO org.spring.example.MongoApp: 46 - Number of people = : 0
DEBUG work.data.mongodb.core.MongoTemplate: 376 - Dropped collection [database.person]
public class ReactiveMongoApplication {
private static final Logger log = LoggerFactory.getLogger(ReactiveMongoApplication.class);
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
ReactiveMongoTemplate template = new ReactiveMongoTemplate(MongoClients.create(), "database");
template.insert(new Person("Joe", 34)).doOnNext(person -> log.info("Insert: " + person))
.flatMap(person -> template.findById(person.getId(), Person.class))
.doOnNext(person -> log.info("Found: " + person))
.zipWith(person -> template.updateFirst(query(where("name").is("Joe")), update("age", 35), Person.class))
.flatMap(tuple -> template.remove(tuple.getT1())).flatMap(deleteResult -> template.findAll(Person.class))
.count().doOnSuccess(count -> {
log.info("Number of people: " + count);
latch.countDown();
})
.subscribe();
latch.await();
}
}
MongoConverter 通过(按约定)识别 Id 属性名,实现了 String 与数据库中存储的 ObjectId 之间的隐式转换。
前述示例旨在展示 MongoTemplate / ReactiveMongoTemplate 上保存、更新和移除操作的用法,而非展示复杂的映射功能。前述示例中所用的查询语法在「查询文档」一节中有更详细的说明。
MongoDB 要求所有文档都必须有一个 _id 字段。有关此字段特殊处理的详细信息,请参阅 ID 处理 一节。
MongoDB 集合可以包含代表多种类型实例的文档。详细信息请参阅类型映射。
插入 / 保存
MongoTemplate 上有多个便捷方法用于保存和插入您的对象。若要对转换过程进行更细粒度的控制,您可以向 MappingMongoConverter 注册 Spring 转换器——例如 Converter<Person, Document> 和 Converter<Document, Person>。
插入操作与保存操作的区别在于:如果对象尚不存在,保存操作会执行插入。
使用保存操作最简单的情形是保存一个 POJO。在这种情况下,集合名称由类的名称(非全限定名)决定。您也可以在调用保存操作时指定一个具体的集合名称。您可以使用映射元数据来覆盖用于存储对象的集合。
在插入或保存时,如果未设置 Id 属性,则假定其值将由数据库自动生成。因此,要使 ObjectId 的自动生成成功,您类中 Id 属性或字段的类型必须是 String、ObjectId 或 BigInteger。
以下示例展示了如何保存文档并检索其内容:
- Imperative
- Reactive
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();
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Criteria.query;
//...
Mono<Person> person = mongoTemplate.insert(new Person("Bob", 33))
.then(mongoTemplate.query(Person.class)
.matching(query(where("age").is(33)))
.one());
以下插入和保存操作可用:
-
voidsave(Object objectToSave): 将对象保存到默认集合。 -
voidsave(Object objectToSave, String collectionName): 将对象保存到指定集合。
也有一组类似的插入操作可用:
-
voidinsert(Object objectToSave): 将对象插入到默认集合。 -
voidinsert(Object objectToSave, String collectionName): 将对象插入到指定集合。
映射层如何处理 _id 字段
MongoDB 要求所有文档都必须有一个 _id 字段。如果您未提供,驱动程序会分配一个带生成值的 ObjectId,而不考虑您的领域模型,因为服务器并不感知您的标识符类型。当您使用 MappingMongoConverter 时,某些规则会支配 Java 类中的属性如何映射到此 _id 字段:
-
用
@Id(org.springframework.data.annotation.Id)注解的属性或字段映射到_id字段。 -
未加注解但名为
id的属性或字段映射到_id字段。
下面概述了在使用 MappingMongoConverter(MongoTemplate 的默认转换器)时,对映射到 _id 文档字段的属性所做的类型转换(如果有的话)。
-
如果可能,Java 类中声明为
String的id属性或字段会通过 Spring 的Converter<String, ObjectId>转换为ObjectId并存储。有效的转换规则委托给 MongoDB Java 驱动程序。如果无法转换为ObjectId,则该值以字符串形式存储在数据库中。 -
声明为
Date的id属性或字段会转换为ObjectId并存储。 -
Java 类中声明为
BigInteger的id属性或字段会通过 Spring 的Converter<BigInteger, ObjectId>转换为ObjectId并存储。
如果 Java 类中不存在前述各组规则所指定的字段或属性,驱动程序会生成一个隐式的 _id 字段,但不会将其映射到 Java 类的任何属性或字段。
在查询和更新时,MongoTemplate 使用与保存文档时上述规则相对应的转换器,以便您查询中使用的字段名和类型能够与领域类中的一致。
有些环境需要一种定制化方式来映射 Id 值,例如存储在 MongoDB 中、未经过 Spring Data 映射层处理的数据。文档可以包含既可表示为 ObjectId、也可表示为 String 的 _id 值。从存储中把文档读回领域类型完全没有问题。但由于隐式的 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
}
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、bulkWrite(需要 MongoDB 8.0+)或专用的 BulkOperations 接口支持此功能。
- Imperative
- Reactive
Collection<Person> inserted = template.insert(List.of(...), Person.class);
Flux<Person> inserted = template.insert(List.of(...), Person.class);
批量写入
BulkWrite 允许您使用单个请求执行插入、更新和删除操作。
MongoDB 8.0 引入了跨多个集合的 BulkWrite。
当您需要在单个请求中混合执行插入、更新和删除操作,并可选地跨多个集合时,请使用 bulkWrite(Bulk, BulkWriteOptions)(MongoDB 8.0+)。如果您针对单个集合,那么即使在较旧的 MongoDB 服务器版本上,仍可以利用新的 bulkWrite 方法,或者继续使用经典的 bulkOps(BulkMode, Class) API。
- Imperative
- Reactive
Bulk bulk = Bulk.create(builder -> builder
.inCollection(Person.class, spec -> spec
.insert(person1)
.insert(person2)
.upsert(where("_id").is("..."), new Update().set("firstname", "...")))
.inCollection(User.class, spec -> spec.insert(new User("..."))));
BulkWriteResult result = template.bulkWrite(bulk, BulkWriteOptions.ordered());
Bulk bulk = Bulk.create(builder -> builder
.inCollection(Person.class, spec -> spec
.insert(person1)
.insert(person2)
.upsert(where("_id").is("..."), new Update().set("firstname", "...")))
.inCollection(User.class, spec -> spec.insert(new User("..."))));
template.bulkWrite(bulk, BulkWriteOptions.ordered()).subscribe();
- Imperative
- Reactive
BulkWriteResult result = template.bulkOps(BulkMode.ORDERED, Person.class)
.insert(List.of(...))
.execute();
Mono<BulkWriteResult> result = template.bulkOps(BulkMode.ORDERED, Person.class)
.insert(List.of(...))
.execute();
对于批处理和批量操作中的各个操作,服务器性能是相同的。不过,批量操作由于可以在单个请求上执行,因此消耗的资源更少。生命周期事件 的发布在批量操作中受到限制。
在调用 insert 之前尚未设置的任何 @Version 属性都会被自动初始化:对于 int 之类的简单类型初始化为 1,对于包装类型(例如 Integer)初始化为 0。
更多信息请参阅乐观锁一节。
更新
对于更新,您可以使用 MongoOperations.updateFirst 更新找到的第一个文档,也可以使用 MongoOperations.updateMulti 方法或流式 API 上的 all 更新所有匹配查询的文档。以下示例展示了对所有 SAVINGS 账户的更新:我们使用 $inc 操作符向余额一次性添加 50.00 美元奖金:
- Imperative
- Reactive
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();
import static org.springframework.data.mongodb.core.query.Criteria.where;
import org.springframework.data.mongodb.core.query.Update;
// ...
Mono<UpdateResult> result = template.update(Account.class)
.matching(where("accounts.accountType").is(Type.SAVINGS))
.apply(new Update().inc("accounts.$.balance", 50.00))
.all();
除了前面讨论的 Query 之外,我们还通过 Update 对象来提供更新定义。Update 类提供了与 MongoDB 可用更新修饰符相对应的方法。大多数方法都会返回 Update 对象,从而为 API 提供流式风格。
如果 Update 中未包含 @Version 属性,它会自动递增。更多信息请参阅乐观锁一节。
对文档执行更新的方法
-
updateFirst:用更新后的文档更新第一个匹配查询文档条件的文档。
-
updateMulti:用更新后的文档更新所有匹配查询文档条件的对象。
对于低于 8.0 的 MongoDB 版本,updateFirst 不支持排序。如果运行的是较旧的版本,请使用 findAndModify 来应用 Sort。
可以通过 Query.withHint(…) 为更新操作提供索引提示。
Update 类中的方法
您可以在 Update 类上使用一点「语法糖」,因为它的方法本就是设计为链式调用的。此外,您可以使用 public static Update update(String key, Object value) 并配合静态导入,来快速创建一个新的 Update 实例。
Update 类包含以下方法:
-
UpdateaddToSet(String key, Object value)使用$addToSet更新修饰符进行更新 -
UpdatecurrentDate(String key)使用$currentDate更新修饰符进行更新 -
UpdatecurrentTimestamp(String key)使用带$typetimestamp的$currentDate更新修饰符进行更新 -
Updateinc(String key, Number inc)使用$inc更新修饰符进行更新 -
Updatemax(String key, Object max)使用$max更新修饰符进行更新 -
Updatemin(String key, Object min)使用$min更新修饰符进行更新 -
Updatemultiply(String key, Number multiplier)使用$mul更新修饰符进行更新 -
Updatepop(String key, Update.Position pos)使用$pop更新修饰符进行更新 -
Updatepull(String key, Object value)使用$pull更新修饰符进行更新 -
UpdatepullAll(String key, Object[] values)使用$pullAll更新修饰符进行更新 -
Updatepush(String key, Object value)使用$push更新修饰符进行更新 -
UpdatepushAll(String key, Object[] values)使用$pushAll更新修饰符进行更新 -
Updaterename(String oldName, String newName)使用$rename更新修饰符进行更新 -
Updateset(String key, Object value)使用$set更新修饰符进行更新 -
UpdatesetOnInsert(String key, Object value)使用$setOnInsert更新修饰符进行更新 -
Updateunset(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");
聚合管道更新
MongoOperations 和 ReactiveMongoOperations 暴露的更新方法还可以通过 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
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
)
第一个
$set阶段根据 tests 字段的平均值计算出一个新字段 average。第二个
$set阶段根据第一个聚合阶段计算出的 average 字段计算出一个新字段 grade。该管道在 students 集合上运行,并使用
Student进行聚合字段映射。将更新应用于集合中所有匹配的文档。
更新插入
与执行 updateFirst 操作相关,您还可以执行 upsert 操作:如果未找到匹配查询的文档,它将执行插入。被插入的文档是查询文档与更新文档的组合。以下示例展示了如何使用 upsert 方法:
- Imperative
- Reactive
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();
Mono<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();
upsert 不支持排序。请使用 findAndModify 来应用 Sort。
如果 Update 中未包含 @Version 属性,它会被自动初始化。更多信息请参阅乐观锁一节。
替换集合中的文档
通过 MongoTemplate 可用的各种 replace 方法允许覆盖第一个匹配的文档。如果未找到匹配项,可以通过提供带有相应配置的 ReplaceOptions 来更新插入一个新文档(如上一节所述)。
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
插入一个新文档。
用于标识要替换的单个文档的查询。
准备替换文档,它必须要么与现有文档具有相同的
_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
对于 upsert,必须提供
_id值,否则 MongoDB 会创建一个新的、可能与领域类型不兼容的ObjectId。由于 MongoDB 并不感知您的领域类型,任何@Field(targetType)提示都不会被考虑,由此产生的ObjectId可能与您的领域模型不兼容。如果未找到匹配项,使用
upsert插入一个新文档
无法通过替换操作更改现有文档的 _id。在 upsert 时,MongoDB 使用两种方式确定该条目的新 id:* 在查询中使用的 _id,如 {"_id" : 1234 };* 替换文档中存在的 _id。如果两种方式都未提供 _id,MongoDB 会为文档创建一个新的 ObjectId。如果所用领域类型的 id 属性是另一种类型(例如 Long),这可能会导致映射和数据查找出错。
查找并修改
MongoCollection 上的 findAndModify(…) 方法可以在单次操作中更新一个文档,并返回旧文档或新更新的文档。MongoTemplate 提供了四个 findAndModify 重载方法,它们接收 Query 和 Update 类,并将 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);
以下示例向容器中插入几个 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
FindAndModifyOptions 方法让您可以设置 returnNew、upsert 和 remove 等选项。下面是从前述代码片段扩展而来的一个示例:
Person upserted = template.update(Person.class)
.matching(new Query(Criteria.where("firstName").is("Mary")))
.apply(update)
.withOptions(FindAndModifyOptions.options().upsert(true).returnNew(true))
.findAndModifyValue()
如果 Update 中未包含 @Version 属性,它会自动递增。更多信息请参阅乐观锁一节。
查找并替换
替换整个 Document 最直接的方法是使用 save 方法按其 id 进行。然而这未必总是可行。findAndReplace 提供了另一种方式,允许通过一个简单查询来标识要替换的文档。
示例 3. 查找并替换文档
Optional<User> result = template.update(Person.class) 1
.matching(query(where("firstname").is("Tom"))) 2
.replaceWith(new Person("Dick"))
.withOptions(FindAndReplaceOptions.options().upsert()) 3
.as(User.class) 4
.findAndReplace(); 5
使用流式更新 API,并给定领域类型以映射查询并派生集合名称;或者直接使用
MongoOperations#findAndReplace。针对给定领域类型映射后的实际匹配查询。通过查询提供
sort、fields和collation设置。额外的可选钩子,用于提供默认值之外的选项,例如
upsert。用于映射操作结果的可选投影类型。如果未给出,则使用初始领域类型。
触发实际处理。使用
findAndReplaceValue以获取可空结果,而不是一个Optional。
请注意,替换文档本身不得带有 id,因为现有 Document 的 id 会由存储自身携带到替换文档中。另外请记住,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
从关联集合中移除由其
_id指定的单个实体。从
GOT集合中移除所有匹配查询条件的文档。移除
GOT集合中的前三个文档。与 <2> 不同,待移除文档先通过运行给定查询、应用sort、limit和skip选项、按其_id标识出来,然后在一个单独步骤中一次性全部移除。从
GOT集合中移除所有匹配查询条件的文档。与 <3> 不同,文档不是批量删除,而是逐个删除。移除
GOT集合中的前三个文档。与 <3> 不同,文档不是批量删除,而是逐个删除。
乐观锁
在 MongoDB 的上下文中,@Version 注解提供了与 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>
初始插入文档。
version被设置为0。加载刚插入的文档。
version仍为0。更新
version = 0的文档。设置lastname并将version提升为1。尝试更新先前加载、仍为
version = 0的文档。由于当前version为1,该操作会因OptimisticLockingFailureException而失败。
MongoTemplate 上只有某些 CRUD 操作会考虑并更改版本属性。详细信息请查阅 MongoOperations 的 JavaDoc。
乐观锁需要服务器给出写入确认(写入结果响应)。使用 WriteConcern.UNACKNOWLEDGED 可能会导致 OptimisticLockingFailureException 被静默吞掉。
从 2.2 版本起,MongoOperations 在从数据库移除实体时也会带上 @Version 属性。若要在不进行版本检查的情况下移除 Document,请使用 MongoOperations#remove(Query,…),而不是 MongoOperations#remove(Object)。
从 2.2 版本起,仓库在移除带版本号的实体时会检查已确认删除的结果。如果带版本号的实体无法通过 CrudRepository.delete(Object) 删除,则会抛出 OptimisticLockingFailureException。在这种情况下,说明版本已被更改,或者该对象在此期间已被删除。请使用 CrudRepository.deleteById(ID) 来绕过乐观锁功能,不考虑对象的版本直接删除它。