MongoTemplate —保存、更新和删除文档

MongoTemplate 让你保存、更新和删除你的 domain 对象,并将这些对象映射到存储在MongoDB中的文档。

Consider the following class:

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

MongoOperations 是 MongoTemplate 实现的接口。

package org.spring.example;

public class MongoApp {

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

  public static void main(String[] args) {

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

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

    // Insert is used to initially store the object into the database.
    mongoOps.insert(p);
    log.info("Insert: " + p);

    // Find
    p = mongoOps.findById(p.getId(), Person.class);
    log.info("Found: " + p);

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

    // Delete
    mongoOps.remove(p);

    // Check that deletion worked
    List people =  mongoOps.findAll(Person.class);
    log.info("Number of people = : " + people.size());


    mongoOps.dropCollection(Person.class);
  }
}

前面的例子会产生以下日志输出(包括来自 MongoTemplate 的 debug 信息)。

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 上保存、更新和删除操作的使用,而不是为了展示复杂的映射功能。

前面的例子中使用的查询语法在 “查询文档” 一节中有更详细的解释。

一、在映射层中如何处理_id字段

MongoDB要求你为所有的文档设置一个 _id 字段。如果你没有提供一个,驱动程序就会分配一个带有生成值的 ObjectId。当你使用 MappingMongoConverter 时,某些规则规定了如何将Java类中的属性映射到这个 _id 字段。

  1. 用 @Id(org.springframework.data.annotation.Id)注解的属性或字段映射到 _id 字段。
  2. 一个没有注解但名为 id 的属性或字段映射到 _id 字段。

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

  1. 如果可能的话,通过使用Spring Converter,将Java类中声明为 String 的 id 属性或字段转换为 ObjectId 并存储。有效的转换规则被委托给MongoDB Java驱动。如果它不能被转换为 ObjectId,那么该值将作为字符串存储在数据库中。
  2. 通过使用Spring Converter,在Java类中声明为 BigInteger 的 id 属性或字段被转换为 ObjectId 并存储。

如果Java类中没有前几组规则中指定的字段或属性,驱动程序会生成一个隐含的 _id 文档,但不会映射到Java类的某个属性或字段。

在查询和更新时,MongoTemplate 使用与前面保存文档的规则相对应的转换器(converter),以便在查询中使用的字段名和类型能够与你的domain类中的内容相匹配。

有些环境需要使用自定义的方法来映射 Id 值,例如存储在 MongoDB 中的数据没有经过Spring Data映射层。文档可以包含 _id 值,可以用 ObjectId 或 String 表示。从存储区读回 domain 类型的文档工作得很好。由于隐含的 ObjectId 转换,通过其 id 查询文档会很麻烦。因此不能以这种方式检索文档。对于这些情况,@MongoId 提供了对实际 id 映射尝试的更多控制。

Example 58. @MongoId 映射

public class PlainStringId {
  @MongoId String id; ⑴
}

public class PlainObjectId {
  @MongoId ObjectId id; ⑵
}

public class StringToObjectId {
  @MongoId(FieldType.OBJECT_ID) String id; ⑶
}

⑴id 被视为 String,无需进一步转换。

⑵id 被视为 `ObjectId'。

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

二、 类型映射

MongoDB 集合(collection)可以包含代表各种类型实例的文档。如果你存储了一个类的层次结构,或者有一个具有 Object 类型属性的类,那么这个特性就很有用。在后一种情况下,当检索对象时,必须正确地读入该属性内的值。

为了实现这一点,MappingMongoConverter 使用一个 MongoTypeMapper 抽象,以 DefaultMongoTypeMapper 作为其主要实现。它的默认行为是全路径的 classname 存储在文档内的 _class 下。类型提示是为顶级文档以及每个值(如果它是一个复杂类型和声明的属性类型的子类型)编写的。

Example 59. 类型映射

class Sample {
  Contact value;
}

abstract class Contact { … }

class Person extends Contact { … }

Sample sample = new Sample();
sample.value = new Person();

mongoTemplate.save(sample);

{
  "value" : { "_class" : "com.acme.Person" },
  "_class" : "com.acme.Sample"
}

Spring Data MongoDB 将类型信息作为实际 root 类以及嵌套类型的最后一个字段来存储(因为它是复杂的,是 Contact 的一个子类型)。因此,如果你现在使用 mongoTemplate.findAll(Object.class, "sample"),你可以发现存储的文档是一个 Sample 实例。你还可以发现值属性实际上是一个 Person。

自定义类型映射

如果你想避免把整个Java类的名字写成类型信息,而是想使用一个key,你可以在实体类上使用 @TypeAlias 注解。如果你需要进一步定制映射,可以看看 TypeInformationMapper 接口。该接口的实例可以在 DefaultMongoTypeMapper 上配置,反过来,可以在 MappingMongoConverter 上配置。

Example 60. 为一个实体定义一个类型别名

@TypeAlias("pers")
class Person {

}

请注意,所产生的文档中包含 pers 作为 _class 字段的值。

类型别名只有在映射上下文知道实际类型的情况下才起作用。所需的实体元数据要么在第一次保存时确定,要么必须通过配置的初始实体集提供。默认情况下,配置类会扫描 base package 以寻找潜在的候选者。

@Configuration
class AppConfig extends AbstractMongoClientConfiguration {
@Override
protected Set> getInitialEntitySet() {
return Collections.singleton(Person.class);
}
// ...
}

配置自定义类型映射

下面的例子显示了如何在 MappingMongoConverter 中配置一个自定义的 MongoTypeMapper。

class CustomMongoTypeMapper extends DefaultMongoTypeMapper {
  //implement custom type mapping here
}

Example 61. 配置自定义的 MongoTypeMapper

Java

XML

@Configuration
class SampleMongoConfiguration extends AbstractMongoClientConfiguration {

  @Override
  protected String getDatabaseName() {
    return "database";
  }

  @Bean
  @Override
  public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory,
			MongoCustomConversions customConversions, MongoMappingContext mappingContext) {
    MappingMongoConverter mmc = super.mappingMongoConverter();
    mmc.setTypeMapper(customTypeMapper());
    return mmc;
  }

  @Bean
  public MongoTypeMapper customTypeMapper() {
    return new CustomMongoTypeMapper();
  }
}

请注意,前面的例子扩展了 AbstractMongoClientConfiguration 类,并覆写了 MappingMongoConverter 的 bean 定义,我们在那里配置了我们的自定义 MongoTypeMapper。

三、保存和插入文档的方法

MongoTemplate 上有几个方便的方法,用于保存和插入你的对象。为了对转换过程有更精细的控制,你可以用 MappingMongoConverter 注册 Spring Converter — 例如 Converter 和 Converter

插入(insert)和保存(save)操作之间的区别是,如果对象还没有持久化,保存操作会执行插入操作。

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

当插入或保存时,如果 Id 属性没有被设置,假设它的值将由数据库自动生成。因此,为了成功地自动生成一个 ObjectId,你的类中的 Id 属性或字段的类型必须是一个 String,一个 ObjectId,或一个 BigInteger。

下面的例子显示了如何保存一个文档和检索其内容。

Example 62. 使用 MongoTemplate 插入和检索文档

…

Person p = new Person("Bob", 33);
mongoTemplate.insert(p);

Person qp = mongoTemplate.findOne(query(where("age").is(33)), Person.class);

可以进行以下插入和保存操作。

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

也有一套类似的插入操作。

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

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

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

插入或保存单个对象

MongoDB 驱动支持在单个操作中插入一个文档集合。MongoOperations 接口中的下列方法支持这一功能。

  • insert: 插入一个对象。如果有一个具有相同 id 的现有文档,就会产生一个错误。
  • insertAll: 接受一个 Collection 的集合作为第一个参数。该方法检查每个对象,并根据前面指定的规则将其插入到适当的集合中。
  • save: 保存对象,覆盖任何可能有相同 id 的对象。

批量插入对象

MongoDB驱动支持在一个操作中插入一个文档集合。MongoOperations 接口中的下列方法支持这一功能。

  • insert 方法: 以一个 Collection 作为第一个参数。它们在一次批量写入数据库中插入一个对象的列表。

四、更新集合中的文档

对于更新,你可以通过使用 MongoOperation.updateFirst 来更新找到的第一个文档,或者你可以通过使用 MongoOperation.updateMulti 方法来更新所有找到的符合查询的文档。下面的例子显示了对所有 SAVINGS 账户的更新,我们通过使用 $inc 操作符在余额中添加一次性的50美元奖金。

Example 63. 通过使用 MongoTemplate 更新文档

...

WriteResult wr = mongoTemplate.updateMulti(new Query(where("accounts.accountType").is(Account.Type.SAVINGS)),
  new Update().inc("accounts.$.balance", 50.00), Account.class);

除了前面讨论的 Query 之外,我们还通过使用 Update 对象来提供更新定义。 Update 类拥有与 MongoDB 可用的更新修改器(update modifier)相匹配的方法。 大多数方法都返回 Update 对象,为 API 提供一种 fluent 的风格。

运行更新文档的方法

  • updateFirst: 用更新后的文档更新第一个符合查询文档 criteria 的文档。
  • updateMulti: 用更新后的文档更新所有符合查询文档 criteria 的对象。

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

Update类中的方法

你可以在 Update 类中使用一点 "'语法糖'",因为它的方法是要被串联起来的。另外,你可以通过使用 public static Update update(String key, Object value) 和使用静态导入来启动一个新的 Update 实例的创建。

Update 类包含以下方法。

  • Update addToSet (String key, Object value) 使用 $addToSet 更新修改器(update modifier)进行更新
  • Update currentDate (String key) 使用 $currentDate 更新修改器进行更新
  • Update currentTimestamp (String key) 使用 $currentDate 更新修改器与 $type timestamp 进行更新
  • 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");

五、在一个集合中 “Upsert” 文档

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

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。

六、查和 Upsert 集合中的文档

MongoCollection 上的 findAndModify(…​) 方法可以更新一个文档,并在一次操作中返回旧的或新更新的文档。MongoTemplate 提供了四个 findAndModify 重载方法,这些方法采用 Query 和 Update 类,并从 Document 转换为你的POJO。

 T findAndModify(Query query, Update update, Class entityClass);

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

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

 T findAndModify(Query query, Update update, FindAndModifyOptions options, Class 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(); // return's old person object

assertThat(oldValue.getFirstName()).isEqualTo("Harry");
assertThat(oldValue.getAge()).isEqualTo(23);

Person newValue = template.query(Person.class)
  .matching(query)
  .findOneValue();

assertThat(newValue.getAge()).isEqualTo(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();

assertThat(newestValue.getAge()).isEqualTo(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()

assertThat(upserted.getFirstName()).isEqualTo("Mary");
assertThat(upserted.getAge()).isOne();

七、聚合管道(Aggregation Pipeline)更新

MongoOperations 和 ReactiveMongoOperations 暴露的更新方法也通过 AggregationUpdate 接受一个 Aggregation Pipeline(聚合管道)。使用 AggregationUpdate 可以在更新操作中利用 MongoDB 4.2 聚合。在更新中使用聚合允许通过用单个操作表达多个阶段和多个条件来更新一个或多个字段。

更新可以包括以下几个阶段。

  • AggregationUpdate.set(…​).toValue(…​) → $set : { …​ }
  • AggregationUpdate.unset(…​) → $unset : [ …​ ]
  • AggregationUpdate.replaceWith(…​) → $replaceWith : { …​ }

Example 64. Update Aggregation

AggregationUpdate update = Aggregation.newUpdate()
    .set("average").toValue(ArithmeticOperators.valueOf("tests").avg())     ⑴
    .set("grade").toValue(ConditionalOperators.switchCases(                 ⑵
        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)                                              
    .apply(update)    ⑶
    .all();                     ⑷                                            
db.students.update(                                                         ⑶
   { },
   [
     { $set: { average : { $avg: "$tests" } } },                       ⑴     
     { $set: { grade: { $switch: {                                         ⑵ 
                           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 }                                                          ⑷
)

⑴第1个 $set 阶段根据 tests 字段的平均值(average)计算出一个新的字段 average。

⑵第2个 $set 阶段根据第一聚合阶段计算的 average 字段,计算新的字段 grade。

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

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

八、查和替换文档

替换整个 Document 的最直接的方法是通过其 id 使用 save 方法。 findAndReplace 提供了一个替代方法,允许通过一个简单的查询来确定要替换的文档。

Example 65. 查找和替换文档

Optional result = template.update(Person.class)      
    .matching(query(where("firstame").is("Tom")))          ⑴
    .replaceWith(new Person("Dick"))⑵
    .withOptions(FindAndReplaceOptions.options().upsert()) ⑶
    .as(User.class)                            ⑷            
    .findAndReplace();                    ⑸                 

⑴使用给定的 domain 类型的fluent更新API来映射查询并推导出集合名称,或者直接使用 MongoOperations#findAndReplace。

⑵针对给定的 domain 类型映射的实际匹配查询。通过查询提供 sort、fields 和 collation 设置。

⑶额外的可选 hook,提供除默认值以外的选项,如 upsert。

⑷用于映射操作结果的可选投影类型。如果没有,则使用初始 domain 类型。

⑸触发实际处理。使用 findAndReplaceValue 来获得可能为 null 的结果,而不是一个 Optional。

请注意,替换的文档本身不能持有一个 id,因为现有的 Document 的 id 会被store本身带入替换的文档中。另外请记住,findAndReplace 只会根据可能给定的排序顺序,替换符合查询条件的第一个文档。

九、删除文档的方法

你可以使用五个重载方法中的一个来从数据库中删除一个对象。

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

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

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

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

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

⑴从关联的集合中移除由其 _id 指定的单个实体。

⑵从 GOT 集合中删除所有符合查询条件的文文档。

⑶删除 GOT 集合中的前三个文档。与 <2> 不同的是,要删除的文档是通过它们的 _id 来识别的,运行给定的查询,先应用 sort、limit 和 skip 选项,然后在一个单独的步骤中一次性删除所有的文档。

⑷从 GOT 集合中删除所有符合查询条件的文档。与<3>不同,文档不会被批量删除,而是一个一个地删除。

⑸删除 GOT 集合中的前三个文档。与 <3> 不同,文档不会被批量删除,而是一个一个地删除。

十、乐观锁

@Version 注解提供了类似于 JPA 在 MongoDB 上下文中的语法,并确保更新只应用于具有匹配 version 的文档。因此,version 属性的实际值被添加到更新查询中,如果在此期间另一个操作改变了文档,那么更新就不会有任何影响。在这种情况下,会抛出一个 OptimisticLockingFailureException。下面的例子显示了这些特征。

@Document
class Person {

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

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

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

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

template.save(tmp); // throws OptimisticLockingFailureException         ⑷    

⑴部分插入文档。 version 设置为 0。

⑵加载刚插入的文档。version 仍为 0。

⑶用 version = 0 来更新文件。设置 lastname 并将 version 自增到 1。

⑷尝试更新先前加载的文件,该文件仍然是 version = 0。由于当前的 version 是 1,操作失败,出现了 OptimisticLockingFailureException。

乐观锁要求将 WriteConcern 设置为 ACKNOWLEDGED。否则, OptimisticLockingFailureException 会被吞掉。

从 2.2 版开始,MongoOperations 在从数据库中删除实体时也包括 @Version 属性。要删除一个没有版本检查的 Document,请使用 MongoOperations#remove(Query,…​) 而不是 MongoOperations#remove(Object)。

从 2.2 版本开始,repository 在删除有 version 实体时检查确认的删除结果。如果一个有 version 的实体不能通过 CrudRepository.delete(Object) 被删除,就会引发一个 OptimisticLockingFailureException。在这种情况下,version 被改变了,或者该对象在这期间被删除了。使用 CrudRepository.deleteById(ID) 可以绕过乐观锁特性,删除对象而不考虑其版本。

你可能感兴趣的:(MongoDB,mongodb)