C#驱动程序MongoDb:如何使用“ UpdateOneAsync”

时间:2019-10-01 21:09:25

标签: c# mongodb-.net-driver

我只想更新文档的单个字段。我的意思是理解我必须使用UpdateOneAsync。当我尝试执行此操作时,我总是得到t MongoDB.Bson.BsonSerializationException : Element name 'Test' is not valid'.

以下代码重现了我的问题(xUnit,.NET Core,Docker中的MongoDb)。

public class Fixture
{
    public class DummyDocument : IDocument
    {
        public string Test { get; set; }

        public Guid Id { get; set; }

        public int Version { get; set; }
    }

    [Fact]
    public async Task Repro()
    {
        var db = new MongoDbContext("mongodb://localhost:27017", "myDb");
        var document = new DummyDocument { Test = "abc", Id = Guid.Parse("69695d2c-90e7-4a4c-b478-6c8fb2a1dc5c") };
        await db.GetCollection<DummyDocument>().InsertOneAsync(document);
        FilterDefinition<DummyDocument> u = new ExpressionFilterDefinition<DummyDocument>(d => d.Id == document.Id);
        await db.GetCollection<DummyDocument>().UpdateOneAsync(u, new ObjectUpdateDefinition<DummyDocument>(new DummyDocument { Test = "bla" }));
    }
}

2 个答案:

答案 0 :(得分:2)

最近,出于学习目的,我已经尝试在自己的项目中实施UpdateOneAsync方法,并相信我所获得的见解可能对msallin等人有所帮助。我还涉及UpdateManyAsync的用法。

msallin的帖子中,我得到一种印象,即所需的功能围绕更新给定对象类的一个或多个字段而展开。 crgolden的注释(及其中的注释)证实了这一点,表示需要不替换整个文档(在数据库内),而是替换与存储在其中的对象类有关的字段。还需要了解ObjectUpdateDefinition的用法,我不熟悉它,不能直接使用。因此,我将展示如何实现以前的功能。

为了涵盖msallin的潜在用例,我将展示如何更新父类和子类上的字段。撰写本文时,所显示的每个示例均已运行,并能正常工作。

连接信息(使用MongoDB.Driver v2.9.2)以及父级和子级类结构:

connectionString = "mongodb://localhost:27017";
client = new MongoClient(connectionString);
database = client.GetDatabase("authors_and_books");      // rename as appropriate
collection = database.GetCollection<Author>("authors");  // rename as appropriate

public class Author // Parent class
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string LifeExpectancy { get; set; }
    public IList<Book> Books { get; set; }
    = new List<Book>();
}

public class Book // Child class
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public Guid ParentId { get; set; }
}

假设:

  • 数据库(此处为authors_and_books)包含一个名为authors的集合,该集合包含一个或多个Author类对象。
  • 每个Author类对象都包含Book类对象的列表。该列表可以为空,或填充有一个或多个Book类对象。
  • 出于测试目的,集合authors中已经填充了一些Author类对象
  • 任何ParentId对象的Book属性始终与封闭的父类的Id相同。当在Author集合中插入authors对象时,或者例如在用测试数据初始化数据库时,将在存储库级别上对此进行维护。 Guid标识符是父类和子类之间的唯一链接,可能会派上用场。
  • 将新作者添加到集合中时,例如通过使用Id,将生成Author类对象的Guid.NewGuid()属性。
  • 我们假设Book个对象IEnumerable<Book>的列表,希望将其添加到某些作者的Books集合中。我们假设我们想将这些内容添加到已经存在的任何书籍中(如果有),而不是替换已经存在的书籍。其他修改也将进行。
  • 所做的修改旨在显示方法的功能,可能并不总是上下文相关的。
  • 我们使用asyncawait以及Task异步运行这些受I / O约束的操作。与同步运行相比,这可以防止执行线程在多次调用同一方法时被锁定。例如,在声明类方法时,我们使用AddBooksCollection(IEnumerable<Book> bookEntities)来代替async Task AddBooksCollection(IEnumerable<Book> bookEntities)

测试数据:父作者类,用于初始化数据库
用ie将它们插入我们的收藏夹。 InsertManyInsertManyAsync

var test_data = new List<DbAuthor>()
{
    new Author()
    {
        Id = new Guid("6f51ea64-6d46-4eb2-b5d5-c92cdaf3260c"),
        FirstName = "Owen",
        LastName = "King",
        LifeExpectancy = "30 years",
        Version = 1
    },
    new Author()
    {
        Id = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf"),
        FirstName = "Stephen",
        LastName = "Fry",
        LifeExpectancy = "50 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93"),
                Title = "The Shining Doorstep",
                Description = "The amenable kitchen doorsteps kills again..",
                ParentId = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf")
            }
        }
    },
    new Author()
    {
        Id = new Guid("76053df4-6687-4353-8937-b45556748abe"),
        FirstName = "Johnny",
        LastName = "King",
        LifeExpectancy = "13 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215"),
                Title = "A Game of Machines",
                Description = "The book just 'works'..",
                ParentId = new Guid("76053df4-6687-4353-8937-b45556748abe")
            },
            new Book()
            {
                Id = new Guid("bc4c35c3-3857-4250-9449-155fcf5109ec"),
                Title = "The Winds of the Sea",
                Description = "Forthcoming 6th novel in..",
                ParentId = new Guid("76053df4-6687-4353-8937-b45556748abe")
            }
        }
    },
    new Author()
    {
        Id = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3"),
        FirstName = "Nan",
        LastName = "Brahman",
        LifeExpectancy = "30 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("9edf91ee-ab77-4521-a402-5f188bc0c577"),
                Title = "Unakien Godlings",
                Description = "If only crusty bread were 'this' good..",
                ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
            }
        }
    }
};

测试数据:子书类,添加到现有作者中
一些书与其他书具有相同的ParentId。这是故意的。

var bookEntities = new List<Book>()
{
    new Book()
    {
        Id = new Guid("2ee90507-8952-481e-8866-4968cdd87e74"),
        Title = "My First Book",
        Description = "A book that makes you fall asleep and..",
        ParentId = new Guid("6f51ea64-6d46-4eb2-b5d5-c92cdaf3260c")
    },
    new Book()
    {
        Id = new Guid("da89edbd-e8cd-4fde-ab73-4ef648041697"),
        Title = "Book about trees",
        Description = "Forthcoming 100th novel in Some Thing.",
        ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
    },
    new Book()
    {
        Id = new Guid("db76a03d-6503-4750-84d0-9efd64d9a60b"),
        Title = "Book about tree saplings",
        Description = "Impressive 101th novel in Some Thing",
        ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
    },
    new Book()
    {
        Id = new Guid("ee2d2593-5b22-453b-af2f-bcd377dd75b2"),
        Title = "The Winds of the Wind",
        Description = "Try not to wind along..",
        ParentId = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf")
    }
};

直接遍历图书实体
我们遍历可用于插入的Book对象,并在每本书上调用UpdateOneAsync(filter, update)。筛选器可确保仅对标识符值匹配的作者进行更新。通过AddToSet方法,我们可以一次将一本书添加到作者的书籍集中。

await bookEntities.ToAsyncEnumerable().ForEachAsync(async book => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, book.ParentId),
    Builders<Author>.Update.AddToSet(z => z.Books, book)
    ));

遍历已分组的图书实体
现在,我们使用c而不是book来表示ForEachAsync迭代中的书。通过阻止对同一父对象的重复ParentId调用,通过公用UpdateOneAsync将书籍分组为匿名对象可能有助于减少开销。 AddToSetEach方法使我们能够将一本或多本书籍追加到作者的书籍集中,只要这些书籍作为IEnumerable<Book>输入即可。

var bookGroups = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new { key, books });
await bookGroups.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, c.key),
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.books.ToList())
    ));

将图书实体附加到父类对象,并在这些对象上进行迭代
通过直接映射到Author类对象,无需匿名对象即可完成此操作。对得到的IEnumerable<Author>进行迭代,以将书籍添加到不同作者的藏书中。

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new Author() { Id = key, Books = books.ToList() });
await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, c.Id),
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books)
    ));

使用C#LINQ进行简化
请注意,我们可以使用C# LINQ简化上面的某些语句,特别是关于UpdateOneAsync(filter, update)方法中使用的过滤器。下面的示例。

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new Author() { Id = key, Books = books.ToList() });
await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    p => p.Id == c.Id,
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books)
    ));

在同一个过滤的上下文中执行多个更新
通过将它们链接到Builders<Author>.Update.Combine(update01, update02, etc..),可以进行多个更新。第一次更新与以前相同,第二次更新将作者版本增加到1.01,从而将他们与目前没有新书的作者区分开。为了执行更复杂的查询,我们可以扩展在Author调用中创建的GroupBy对象,以携带其他数据。这是在第三次更新中使用的,在第三次更新中,作者的预期寿命根据作者的姓氏而变化。请注意if-else语句,例如(Do you have bananas?) ? if yes : if no

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new DbAuthor()
    {
        Id = key,
        Books = books.ToList(),
        LastName = collection.Find(c => c.Id == key).FirstOrDefault().LastName,
        LifeExpectancy = collection.Find(c => c.Id == key).FirstOrDefault().LifeExpectancy
    });

await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    p => p.Id == c.Id,
    Builders<DbAuthor>.Update.Combine(
        Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books),
        Builders<Author>.Update.Inc(z => z.Version, 0.01),
        Builders<Author>.Update.Set(z => z.LifeExpectancy,
            (c.LastName == "King") ? "Will live forever" : c.LifeExpectancy)
        )));

使用UpdateManyAsync
由于在运行ForEachAsync迭代之前之前已知任何给定作者的命运,因此我们可以专门查询该匹配项,而仅更改那些匹配项(而不必在匹配项上调用Set我们不希望更改的内容)。然后,可以根据需要从上述迭代中排除此设置操作,并使用UpdateManyAsync单独使用。我们使用与上述相同的authors变量。由于不使用C# LINQ与不使用Builders<Author>.Filter.And(filter01, filter02, etc)之间存在较大差异,因此我在此展示了两种实现方法(选择一种)。我们一次使用多个过滤器。为var authorIds = authors.Select(c => c.Id).ToList(); // Using builders: await collection.UpdateManyAsync( Builders<Author>.Filter.And( Builders<Author>.Filter.In(c => c.Id, authorIds), Builders<Author>.Filter.Eq(p => p.LastName, "King") ), Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever")); // Using C# LINQ: await collection.UpdateManyAsync<Author>(c => authorIds.Contains(c.Id) && c.LastName == "King", Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever"));

UpdateManyAsync

UpdateManyAsync的其他用例
在这里,我展示了将C# LINQ与我们的数据一起使用的其他可能相关的方法。为了使事情紧凑,我决定在本节中专门使用// Shouldn't all authors with lastname "King" live eternally? await collection.UpdateManyAsync<Author>(c => c.LastName == "King", Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever")); // Given a known author id, how do I update the related author? var authorId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3"); await collection.UpdateManyAsync<Author>(c => c.Id == authorId), Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will not live forever")); // Given a known book id, how do I update any related authors? var bookId = new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93"); await collection.UpdateManyAsync<Author>(c => c.Books.Any(p => p.Id == bookId), Builders<Author>.Update.Set(p => p.LifeExpectancy, "Death by doorsteps in due time"));

[-1]

嵌套文档:更新单个子书对象
要直接更新子文档字段,我们使用Mongo DB Positional Operator。在C#中,通过键入$进行声明,这等效于Mongo DB Shell中的[-1]。撰写本文时,驱动程序似乎仅支持使用IList类型的Cannot apply indexing with [] to an expression of type ICollection<Book>。由于出现错误IEnumerable<Book>,使用此运算符最初有些挣扎。尝试使用IList<Book>时出现相同的错误。因此,请确保现在使用// Given a known book id, how do I update only that book? (non-async, also works with UpdateOne) var bookId = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215"); collection.FindOneAndUpdate( Builders<Author>.Filter.ElemMatch(c => c.Books, p => p.Id == bookId), Builders<Author>.Update.Set(z => z.Books[-1].Title, "amazing new title") ); // Given a known book id, how do I update only that book? (async) var bookId = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215"); await collection.UpdateOneAsync( Builders<Author>.Filter.ElemMatch(c => c.Books, p => p.Id == bookId), Builders<Author>.Update.Set(z => z.Books[-1].Title, "here we go again") ); // Given several known book ids, how do we update each of the books? var bookIds = new List<Guid>() { new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93"), new Guid("bc4c35c3-3857-4250-9449-155fcf5109ec"), new Guid("447eb762-95e9-4c31-95e1-b20053fbe215") }; await bookIds.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync( p => p.Books.Any(z => z.Id == c), Builders<Author>.Update.Set(z => z.Books[-1].Title, "new title yup yup") ));

UpdateManyAsync

像上面那样,一次调用UpdateOneAsync而不是三次调用Guid可能很诱人。实现如下所示。尽管此方法有效(调用不会出错),但不会更新所有三本书。由于它一次只能访问每个选定的作者,因此只能对三本书中的两本书进行更新(因为我们故意列出了ParentId个ID等于Guid await collection.UpdateManyAsync( c => c.Books.Any(p => bookIds.Contains(p.Id)), Builders<Author>.Update.Set(z => z.Books[-1].Title, "new title once per author") ); 的书籍)。上面,所有三个更新都可以完成,因为我们对同一作者进行了一次额外的迭代。

{{1}}

结束语
这对您有什么帮助?您是否有任何进一步的查询需求?半个星期前,我开始研究与我的个人项目有关的这些异步方法,所以我希望我介绍的内容与您有关。

您可能还对bulk writes感兴趣,以捆绑您要对数据库进行的更改,并让Mongo Db优化对数据库的调用以进行性能优化。在这方面,我建议您使用广泛的example on bulk operations for performance。此example 也可能是相关的。

答案 1 :(得分:0)

您是否尝试过按照the docs中显示的方式进行操作?我认为对您来说可能是这样的:

[Fact]
public async Task Repro()
{
    var db = new MongoDbContext("mongodb://localhost:27017", "myDb");
    var document = new DummyDocument { Test = "abc", Id = Guid.Parse("69695d2c-90e7-4a4c-b478-6c8fb2a1dc5c") };
    await db.GetCollection<DummyDocument>().InsertOneAsync(document);
    var filter = Builders<DummyDocument>.Filter.Eq("Id", document.Id);
    var update = Builders<DummyDocument>.Update.Set("Test", "bla");
    await db.GetCollection<DummyDocument>().UpdateOneAsync(filter, update);
}

更新:

基于OP注释,要发送整个对象而不是更新特定属性,请尝试ReplaceOneAsync

⋮
var filter = Builders<DummyDocument>.Filter.Eq("Id", document.Id);
await db.GetCollection<DummyDocument>().ReplaceOneAsync(filter, new DummyDocument { Test = "bla" }, new UpdateOptions { IsUpsert = true });