【Spring连载】使用Spring Data访问 MongoDB----Template API之保存、更新和删除Documents

【Spring连载】使用Spring Data访问 MongoDB----Template API之保存、更新和删除Documents

  • 一、Insert / Save
    • 1.1 _id字段在映射层是如何处理的
    • 1.2 我的Documents保存在哪个集合中?
    • 1.3 插入或保存单个对象
    • 1.4 在批中插入多个对象
  • 二、Update
    • 2.1 运行Documents更新的方法
    • 2.2 Update类中的方法
    • 2.3 Aggregation Pipeline更新
  • 三、Upsert
    • 3.1 替换集合中的Documents
  • 四、Find and Modify
  • 五、Find and Replace
  • 六、Delete
  • 七、乐观锁

MongoTemplate / ReactiveMongoTemplatge允许你保存、更新和删除域对象,并将这些对象映射到存储在MongoDB中的documents。命令式和反应式(reactive)API的API签名大部分相同,只是返回类型不同。当同步API使用void、single Object和List时,反应式(reactive)对应地由Mono< Void>, Mono< Object> and 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消息):

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上保存、更新和删除操作的使用,而不是展示复杂的映射功能。前面示例中使用的查询语法在“查询Documents”一文中有更详细的解释。
MongoDB要求所有documents都有一个_id字段。
MongoDB集合可以包含表示各种类型实例的documents。有关详细信息,请参阅类型映射。

一、Insert / Save

MongoTemplate上有几种方便的方法可以保存和插入对象。为了对转换过程进行更细粒度的控制,可以使用MappingMongoConverter注册Spring转换器 — 例如Converter和Converter
插入操作和保存操作的区别在于,如果对象不存在,保存操作将执行插入操作。
使用保存操作的简单情况是保存一个POJO。在这种情况下,集合名称由类的名称(不完全限定)决定。你也可以使用特定的集合名称调用保存操作。可以使用映射元数据来覆盖存储对象的集合。
在插入或保存时,如果未设置Id属性,则假设其值将由数据库自动生成。因此,若要成功自动生成ObjectId,类中的Id属性或字段的类型必须是String、ObjectId或BigInteger。
以下示例显示了如何保存document并检索其内容:
使用MongoTemplate插入和检索documents

//...

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):将对象插入到指定的集合中。

1.1 _id字段在映射层是如何处理的

MongoDB要求所有documents都有一个_id字段。如果你不提供,则driver将为ObjectId分配一个生成的值,而不考虑您的域模型(domain model),因为服务器不知道你的标识符(identifier)类型。当你使用MappingMongoConverter时,有规则控制Java类的属性如何映射到此_id字段:

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

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

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

如果Java类中不存在先前规则集中指定的字段或属性,则driver会生成一个隐式_id文件,但不会映射到Java类的属性或字段。
在查询和更新时,MongoTemplate使用与前面保存documents的规则相对应的转换器,以便在查询中使用的字段名和类型可以与域类中的字段名称和类型相匹配。
有些环境需要一种自定义的方法来映射Id值,例如存储在MongoDB中的数据,这些数据没有通过Spring Data映射层运行。文档可以包含_id值,这些值可以表示为ObjectId或String。将documents从存储读取回域类型也可以。由于隐式的ObjectId转换,通过documents id查询文档可能会很麻烦。因此,无法以这种方式检索documents。对于这些情况,@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
}

1. id被当作String处理,无需进一步转换。
2. id被视为ObjectId3. 如果给定的String是有效的ObjectId十六进制,则id被视为ObjectId,否则作为String。对应于@Id用法。

1.2 我的Documents保存在哪个集合中?

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

1.3 插入或保存单个对象

MongoDB driver支持在一次操作中插入一组documents。MongoOperations接口中的以下方法支持此功能:

  • insert:插入对象。如果存在具有相同id的document,则会生成一个错误。
  • insertAll:将对象集合作为第一个参数。此方法检查每个对象,并根据前面指定的规则将其插入到相应的集合中。
  • save:保存对象,覆盖可能具有相同id的任何对象。

1.4 在批中插入多个对象

MongoDB driver支持在一个操作中插入一个documents集合。MongoOperations接口中的以下方法通过插入或专用BulkOperations接口支持此功能。
Batch Insert

Collection<Person> inserted = template.insert(List.of(...), Person.class);

Bulk Insert

BulkWriteResult result = template.bulkOps(BulkMode.ORDERED, Person.class)
    .insert(List.of(...))
    .execute();

batch 和 bulk的服务器性能是相同的。但是,bulk操作不发布生命周期事件。任何在调用insert之前没有设置的@Version属性将被自动初始化为1(在简单类型的情况下,如int)或包装类型的0,(如Integer)。请参阅8.7 乐观锁部分了解更多信息。

二、Update

对于更新,你可以使用MongoOperation.updateFirst更新找到的第一个文档,也可以使用MongoOperation.updateMulti方法或fluent API上的所有方法更新找到的与查询匹配的所有文档。以下示例展示了所有SAVINGS账户的更新,其中我们使用$inc运算符向余额中添加一次性$50.00奖金:
使用MongoTemplate更新documents

// ...

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提供流畅的样式。
@Version 属性(如果未包含在Update中)将自动递增。请参阅8.7 乐观锁部分了解更多信息。

2.1 运行Documents更新的方法

  • updateFirst:使用更新后的document更新与查询文档条件匹配的第一个document。
  • updateMulti:使用更新的document更新与查询文档条件匹配的所有对象。

updateFirst不支持排序。请使用章节8.4 findAndModify应用排序。
更新操作的索引提示可以通过Query.withHint(…​)提供。

2.2 Update类中的方法

你可以在Update类中使用一点“syntax sugar”,因为它的方法应该被链接在一起。此外,你可以通过使用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)使用带有 t y p e t i m e s t a m p 的 type timestamp的 typetimestampcurrentDate更新修饰符进行更新
  • 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更新修饰符更新

一些更新修饰符,如 p u s h 和 push和 pushaddToSet,允许嵌套其他运算符。

// { $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");

2.3 Aggregation Pipeline更新

MongoOperations和ReactiveMongoOperations公开的更新方法也通过AggregationUpdate接受Aggregation Pipeline。使用AggregationUpdate可以在更新操作中利用MongoDB 4.2 aggregations。通过在更新中使用聚合,可以通过一个操作表达多个阶段和多个条件来更新一个或多个字段。
更新可以包括以下阶段:

  • 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
)

1. 第一个$set阶段根据tests字段的平均值计算新的字段average。
2. 第二个$set阶段基于第一聚合阶段计算的average字段来计算新的字段grade。
3. 管道(pipeline)在students集合上运行,并使用Student进行聚合字段映射。
4. 将更新应用于集合中的所有匹配documents。

三、Upsert

与执行updateFirst操作相关,你还可以执行upsert操作,如果找不到与查询匹配的document,该操作将执行插入。插入的document是查询文档和更新文档的组合。以下示例展示了如何使用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不支持排序。请使用章节8.4、findAndModify应用排序。
@Version属性如果不包括在更新中,将自动初始化。请参阅8.7 乐观锁部分了解更多信息。

3.1 替换集合中的Documents

MongoTemplate提供的各种替换方法允许覆盖第一个匹配的Document。如果没有找到匹配项,则可以通过提供相应配置的ReplaceOptions来upsert一个新的document(如前一节所述)。
替换一个

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. 插入一个新document。
2. 用于标识要替换的单个document的查询。
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. upsert需要有_id值,否则MongoDB将创建一个新的可能具有域类型不兼容的ObjectId的值。由于MongoDB不知道你的域类型,因此不考虑任何@Field(targetType)提示,并且生成的ObjectId可能与你的域模型不兼容。
2. 如果找不到匹配项,使用upsert插入新文档

注意,无法使用替换操作更改现有documents的_id。当upsert时,MongoDB使用两种方法来确定条目的新id:

  • 在查询中使用_id,如{“_id” : 1234 }中所示。
  • 替换document中存在_id。

如果任何一种方式都没有提供_id,MongoDB将为document创建一个新的ObjectId。如果使用的域类型id属性具有不同的类型(例如Long),这可能会导致映射和数据查找故障。

四、Find and Modify

MongoOperations上的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()

@Version属性如果不包含在Update中,将自动增加。请参阅8.7 乐观锁部分了解更多信息。

五、Find and Replace

替换整个Document的最直接的方法是通过其id使用save方法。然而,这可能并不总是可行的。findAndReplace提供了一种替代方法,允许通过简单的查询识别要替换的文档。
例3:查找和替换Documents

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. 使用带有给定域类型的fluent update API来映射查询和派生集合名称,或者只使用MongoOperations#findAndReplace。
2. 实际匹配查询映射到给定的域类型。通过查询提供sort、fields和collation设置。
3. 额外的可选钩子,提供默认选项以外的选项,比如upsert。
4. 用于映射操作结果的可选投影类型。如果没有,则使用初始域类型。
5. 触发实际的处理。使用findAndReplaceValue来获取可空的结果,而不是一个Optional

请注意,replacement必须不包含id本身,因为现有文档的id将由存储本身传递给replacement。还要记住,findAndReplace将仅根据可能给定的排序顺序替换匹配查询条件的第一个document。

六、Delete

你可以使用以下五种重载方法之一从数据库中删除对象:

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集合中删除所有符合查询条件的documents。
3. 删除GOT集合中的前三个documents。与<2>不同的是,要删除的文档由它们的_id标识,运行给定的查询,首先应用sort、limit和skip选项,然后在单独的步骤中一次删除所有documents。
4.GOT集合中删除所有符合查询条件的文档。与<3>不同,documents不会在一批中删除,而是一个接一个地删除。
5. 删除GOT集合中的前三个documents。与<3>不同,documents不会在一批中删除,而是一个接一个地删除。

七、乐观锁

@Version注解提供了MongoDB上下文中类似于JPA的语法,并确保更新仅应用于具有匹配version的文档。因此,将version属性的实际值添加到更新查询的方式是,如果另一个操作同时更改了文档,则更新不会产生任何影响。在这种情况下,将抛出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. 直接插入文档。版本设置为02. 加载刚刚插入的文档。版本仍然是03. 更新版本为0的文档。设置lastname并将版本设置为14. 尝试更新之前加载的版本仍然为0的document。操作失败,出现OptimisticLockingFailureException,因为当前版本为1

只有MongoTemplate上的某些CRUD操作会更改版本属性。有关详细信息,请参阅MongoOperations的java文档。
乐观锁要求将WriteConcern设置为ACKNOWLEDGED。否则,OptimisticLockingFailureException可能会被吞掉(silently swallowed)。
从版本2.2开始,MongoOperations在从数据库中删除实体时也包括@Version属性。要在不进行版本检查的情况下删除Document ,请使用MongoOperations#remove(Query,…​) 而不是MongoOperations#remove(Object)。
从版本2.2起,存储库在删除版本控制的实体时检查确认删除的结果。如果无法通过CrudRepository.delete(Object)删除版本化实体,则会引发OptimisticLockingFailureException。在这种情况下,版本被更改,或者对象在此期间被删除。使用CrudRepository.deleteById(ID)可以绕过乐观锁功能并删除对象(无论其版本如何)。

你可能感兴趣的:(MongoDB,spring,mongodb,数据库)