保存、更新和删除文档
MongoTemplate
/ ReactiveMongoTemplate
允许你保存、更新和删除你的领域对象,并将这些对象映射到存储在 MongoDB 中的文档。命令式和响应式 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
类,你可以保存、更新和删除对象,如下例所示:
- 命令式
- 响应式
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);
}
}
前面的示例将产生以下日志输出(包括来自 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]
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>
。
插入(insert)和保存(save)操作之间的区别在于,如果对象尚不存在,保存操作会执行插入操作。
使用保存操作的简单情况是保存一个 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());
以下是可用的插入和保存操作:
-
void
保存(Object 要保存的对象)
: 将对象保存到默认集合中。 -
void
保存(Object 要保存的对象, String 集合名称)
: 将对象保存到指定的集合中。
类似的插入操作集也同样可用:
-
void
insert(Object objectToSave)
:将对象插入到默认集合中。 -
void
insert(Object objectToSave, String collectionName)
:将对象插入到指定的集合中。
如何处理映射层中的 _id
字段
MongoDB 要求所有文档都必须有一个 _id
字段。如果你没有提供该字段,驱动程序会自动分配一个带有生成值的 ObjectId
,而不会考虑你的领域模型,因为服务器并不知道你的标识符类型。当你使用 MappingMongoConverter
时,某些规则决定了如何将 Java 类中的属性映射到这个 _id
字段:
-
使用
@Id
(org.springframework.data.annotation.Id
)注解的属性或字段将映射到_id
字段。 -
没有注解但名为
id
的属性或字段也将映射到_id
字段。
以下概述了在使用 MappingMongoConverter
(MongoTemplate
的默认转换器)时,映射到 _id
文档字段的属性所进行的类型转换(如果有的话)。
-
如果可能,通过使用 Spring 的
Converter<String, ObjectId>
,Java 类中声明为String
的id
属性或字段会被转换为ObjectId
并存储。有效的转换规则由 MongoDB Java 驱动程序处理。如果无法将其转换为ObjectId
,则该值将以字符串形式存储在数据库中。 -
Java 类中声明为
BigInteger
的id
属性或字段,通过使用 Spring 的Converter<BigInteger, ObjectId>
,会被转换为ObjectId
并存储。
如果在之前的规则集中没有指定 Java 类中的任何字段或属性,驱动程序会生成一个隐式的 _id
文件,但不会将其映射到 Java 类的属性或字段。
在查询和更新时,MongoTemplate
会使用与前面保存文档规则对应的转换器,以确保查询中使用的字段名和类型与域类中的内容匹配。
在某些环境中,需要对 Id
值进行自定义映射,例如存储在 MongoDB 中但未经过 Spring Data 映射层处理的数据。文档可能包含 _id
值,这些值可以表示为 ObjectId
或 String
。将文档从存储中读取回域类型时,一切正常。然而,由于隐式的 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
或专门的 BulkOperations
接口来支持这一功能。
- Imperative
- Reactive
Collection<Person> inserted = template.insert(List.of(...), Person.class);
Flux<Person> inserted = template.insert(List.of(...), Person.class);
- 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
属性将自动初始化为 1
(对于简单类型如 int
)或 0
(对于包装类型如 Integer
)。
详细信息请参见乐观锁部分。
更新
对于更新操作,你可以使用 MongoOperation.updateFirst
来更新找到的第一个文档,或者使用 MongoOperation.updateMulti
方法或流畅 API 中的 all
来更新所有匹配查询的文档。以下示例展示了如何使用 $inc
运算符对所有 SAVINGS
账户进行更新,我们将一次性增加 $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:根据查询文档条件更新所有匹配的对象。
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");
聚合管道更新
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
作为聚合字段映射。将更新应用于集合中所有匹配的文档。
Upsert
与执行 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
的选项来执行 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
插入一个新文档。
用于识别要替换的单个文档的查询。
设置替换文档,该文档必须包含与现有文档相同的
_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
重载方法,这些方法接受 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);
<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
的方法是通过其 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
使用带有给定域类型的流畅更新 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> 不同,要移除的文档由其_id
标识,先运行给定的查询,应用sort
、limit
和skip
选项,然后在单独的步骤中一次性移除所有文档。从
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>
最初插入文档。
version
被设置为0
。加载刚刚插入的文档。
version
仍然是0
。使用
version = 0
更新文档。设置lastname
并将version
增加至1
。尝试更新之前加载的文档,该文档的
version
仍为0
。由于当前version
为1
,操作将失败并抛出OptimisticLockingFailureException
。
只有对 MongoTemplate
进行的某些 CRUD 操作才会考虑并修改版本属性。详细信息请参阅 MongoOperations
的 Java 文档。
乐观锁(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)
可以绕过乐观锁定功能,无论对象的版本如何,都可以删除对象。