跳到主要内容
版本:5.1.1

保存、更新与移除文档

Doubao 中英对照 Saving, Updating, and Removing Documents

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 类,您可以保存、更新和删除该对象,如下例所示:

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]

MongoConverter 通过(按约定)识别 Id 属性名,实现了 String 与数据库中存储的 ObjectId 之间的隐式转换。

前述示例旨在展示 MongoTemplate / ReactiveMongoTemplate 上保存、更新和移除操作的用法,而非展示复杂的映射功能。前述示例中所用的查询语法在「查询文档」一节中有更详细的说明。

important

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

important

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

插入 / 保存​

MongoTemplate 上有多个便捷方法用于保存和插入您的对象。若要对转换过程进行更细粒度的控制,您可以向 MappingMongoConverter 注册 Spring 转换器——例如 Converter<Person, Document> 和 Converter<Document, Person>。

备注

插入操作与保存操作的区别在于:如果对象尚不存在,保存操作会执行插入。

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

在插入或保存时,如果未设置 Id 属性,则假定其值将由数据库自动生成。因此,要使 ObjectId 的自动生成成功,您类中 Id 属性或字段的类型必须是 String、ObjectId 或 BigInteger。

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

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();

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

  • void save (Object objectToSave): 将对象保存到默认集合。

  • void save (Object objectToSave, String collectionName): 将对象保存到指定集合。

也有一组类似的插入操作可用:

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

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

映射层如何处理 _id 字段​

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

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

  2. 未加注解但名为 id 的属性或字段映射到 _id 字段。

下面概述了在使用 MappingMongoConverter(MongoTemplate 的默认转换器)时,对映射到 _id 文档字段的属性所做的类型转换(如果有的话)。

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

  2. 声明为 Date 的 id 属性或字段会转换为 ObjectId 并存储。

  3. 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 接口支持此功能。

Collection<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。

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());
BulkWriteResult result = template.bulkOps(BulkMode.ORDERED, Person.class)
.insert(List.of(...))
.execute();
备注

对于批处理和批量操作中的各个操作,服务器性能是相同的。不过,批量操作由于可以在单个请求上执行,因此消耗的资源更少。生命周期事件 的发布在批量操作中受到限制。

important

在调用 insert 之前尚未设置的任何 @Version 属性都会被自动初始化:对于 int 之类的简单类型初始化为 1,对于包装类型(例如 Integer)初始化为 0。
更多信息请参阅乐观锁一节。

更新​

对于更新,您可以使用 MongoOperations.updateFirst 更新找到的第一个文档,也可以使用 MongoOperations.updateMulti 方法或流式 API 上的 all 更新所有匹配查询的文档。以下示例展示了对所有 SAVINGS 账户的更新:我们使用 $inc 操作符向余额一次性添加 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();

除了前面讨论的 Query 之外,我们还通过 Update 对象来提供更新定义。Update 类提供了与 MongoDB 可用更新修饰符相对应的方法。大多数方法都会返回 Update 对象,从而为 API 提供流式风格。

important

如果 Update 中未包含 @Version 属性,它会自动递增。更多信息请参阅乐观锁一节。

对文档执行更新的方法​

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

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

注意

对于低于 8.0 的 MongoDB 版本,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 进行聚合字段映射。

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

更新插入​

与执行 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();
注意

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

important

如果 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()
important

如果 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。

important

请注意,替换文档本身不得带有 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) 来绕过乐观锁功能,不考虑对象的版本直接删除它。