保存、更新和删除文档
MongoTemplate / ReactiveMongoTemplatge 允许您保存、更新和删除您的域对象,并将这些对象映射到存储在 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);
}
}
前面的示例将产生以下日志输出(包括来自 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。
以下示例展示了如何保存文档并检索其内容。
-
命令式
-
响应式
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属性或字段将使用 SpringConverter<String, ObjectId>转换为ObjectId并存储为ObjectId。有效的转换规则委托给 MongoDB Java 驱动程序。如果无法将其转换为ObjectId,则该值将作为字符串存储在数据库中。 -
在 Java 类中声明为
BigInteger的id属性或字段将使用 SpringConverter<BigInteger, ObjectId>转换为ObjectId并存储为ObjectId。
如果 Java 类中不存在先前规则集中指定的字段或属性,则驱动程序会生成一个隐式_id文件,但不会映射到 Java 类的属性或字段。
在查询和更新时,MongoTemplate使用与保存文档的先前规则相对应的转换器,以便查询中使用的字段名称和类型与域类中的字段名称和类型匹配。
某些环境需要自定义方法来映射Id值,例如存储在 MongoDB 中的数据,这些数据没有经过 Spring Data 映射层处理。文档可以包含_id值,这些值可以表示为ObjectId或String。将存储中的文档读回域类型工作正常。通过其id查询文档可能很麻烦,因为存在隐式ObjectId转换。因此,无法以这种方式检索文档。对于这些情况,@MongoId提供了对实际 id 映射尝试的更多控制。
@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)
}
| 1 | id 被视为String,无需进一步转换。 |
| 2 | id 被视为ObjectId。 |
| 3 | 如果给定的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);
Flux<Person> inserted = template.insert(List.of(...), Person.class);
-
命令式
-
响应式
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();
|
批量和批量操作的服务器性能相同。但是,批量操作不会发布生命周期事件。 |
|
任何在调用插入之前未设置的 |
更新
对于更新,您可以使用MongoOperation.updateFirst 更新找到的第一个文档,或者您可以使用MongoOperation.updateMulti 方法或流式 API 上的all 更新所有找到的匹配查询的文档。以下示例显示了使用$inc 运算符更新所有SAVINGS 帐户,我们向余额中添加一次性 50.00 美元的奖金
MongoTemplate / ReactiveMongoTemplate 更新文档-
命令式
-
响应式
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 的流畅风格。
|
|
用于运行文档更新的方法
-
updateFirst:使用更新后的文档更新与查询文档条件匹配的第一个文档。
-
updateMulti:使用更新后的文档更新与查询文档条件匹配的所有对象。
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)使用$currentDate更新修饰符和$typetimestamp进行更新 -
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 : { … }
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)
)
| 1 | 第一个 $set 阶段根据 tests 字段的平均值计算一个新的字段 average。 |
| 2 | 第二个 $set 阶段根据第一个聚合阶段计算的 average 字段计算一个新的字段 grade。 |
| 3 | 管道在 students 集合上运行,并使用 Student 进行聚合字段映射。 |
| 4 | 将更新应用于集合中所有匹配的文档。 |
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();
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。
|
|
如果 |
替换集合中的文档
通过 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)
| 1 | 插入一个新文档。 |
| 2 | 用于识别要替换的单个文档的查询。 |
| 3 | 设置替换文档,该文档必须包含与现有文档相同的 _id 或根本不包含 _id。 |
| 4 | 运行替换操作。. 使用 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)
| 1 | _id 值需要存在才能进行 upsert,否则 MongoDB 将创建一个新的,可能与域类型不兼容的 ObjectId。由于 MongoDB 不了解您的域类型,因此不会考虑任何 @Field(targetType) 提示,并且生成的 ObjectId 可能与您的域模型不兼容。 |
| 2 | 如果未找到匹配项,请使用 upsert 插入一个新文档 |
|
使用替换操作无法更改现有文档的 |
查找和修改
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()
|
|
查找和替换
替换整个Document 的最直接方法是通过其id 使用save 方法。但是,这可能并不总是可行。findAndReplace 提供了一种替代方法,允许通过简单的查询来识别要替换的文档。
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)
| 1 | 使用流畅的更新 API,使用给定的域类型来映射查询并推导出集合名称,或者直接使用MongoOperations#findAndReplace。 |
| 2 | 针对给定域类型映射的实际匹配查询。通过查询提供sort、fields 和collation 设置。 |
| 3 | 可选的附加钩子,用于提供除默认值之外的选项,例如upsert。 |
| 4 | 用于映射操作结果的可选投影类型。如果没有给出,则使用初始域类型。 |
| 5 | 触发实际处理。使用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)
| 1 | 从关联的集合中删除由其_id指定的单个实体。 |
| 2 | 删除与GOT集合中的查询条件匹配的所有文档。 |
| 3 | 删除GOT集合中的前三个文档。与<2>不同,要删除的文档由其_id标识,首先运行给定的查询,应用sort、limit和skip选项,然后在单独的步骤中一次性删除所有文档。 |
| 4 | 删除与GOT集合中的查询条件匹配的所有文档。与<3>不同,文档不是批量删除,而是逐个删除。 |
| 5 | 删除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)
| 1 | 最初插入文档。version设置为0。 |
| 2 | 加载刚刚插入的文档。version仍然是0。 |
| 3 | 使用version = 0更新文档。设置lastname并将version增加到1。 |
| 4 | 尝试更新先前加载的文档,该文档的version仍然是0。操作失败并抛出OptimisticLockingFailureException异常,因为当前的version是1。 |
只有MongoTemplate上的某些CRUD操作会考虑和更改版本属性。有关详细信息,请参阅MongoOperations的Java文档。
乐观锁需要将WriteConcern设置为ACKNOWLEDGED。否则,OptimisticLockingFailureException可能会被静默地吞掉。
|
从版本 2.2 开始,MongoOperations 在从数据库中删除实体时也会包含@Version 属性。要删除没有版本检查的Document,请使用MongoOperations#remove(Query,…) 而不是MongoOperations#remove(Object)。
|
从版本 2.2 开始,存储库在删除版本化实体时会检查已确认删除的结果。如果无法通过CrudRepository.delete(Object) 删除版本化实体,则会引发OptimisticLockingFailureException。在这种情况下,版本已更改或对象已在同时被删除。使用CrudRepository.deleteById(ID) 来绕过乐观锁功能,并无论其版本如何删除对象。
|