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

msa*_*lin 1 c# mongodb-.net-driver

我只想更新文档的一个字段。我的意思是理解我必须使用UpdateOneAsync. 当我尝试这样做时,我总是得到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" }));
    }
}
Run Code Online (Sandbox Code Playgroud)

Fhy*_*nir 7

最近我尝试将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; }
}
Run Code Online (Sandbox Code Playgroud)

假设:

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

测试数据:父作者类,初始化数据库将
它们插入到我们的集合中。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")
            }
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

测试数据:子书类,添加到现有作者
一些书籍与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")
    }
};
Run Code Online (Sandbox Code Playgroud)

直接迭代书实体
我们迭代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)
    ));
Run Code Online (Sandbox Code Playgroud)

迭代分组的书籍实体
我们现在使用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())
    ));
Run Code Online (Sandbox Code Playgroud)

将书实体附加到父类对象,并在这些对象上进行迭代
这可以在没有匿名对象的情况下通过直接映射到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)
    ));
Run Code Online (Sandbox Code Playgroud)

用 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)
    ));
Run Code Online (Sandbox Code Playgroud)

对同一个过滤上下文执行多次更新
通过将它们链接到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)
        )));
Run Code Online (Sandbox Code Playgroud)

使用 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"));
Run Code Online (Sandbox Code Playgroud)

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"));
Run Code Online (Sandbox Code Playgroud)

嵌套文档:更新单个子书籍对象
为了直接更新子文档字段,我们使用Mongo DB Positional Operator。在 C# 中,它是通过键入 来声明的[-1],这相当于$在 Mongo DB Shell 中。在撰写本文时,驱动程序似乎仅支持使用[-1]IList类型。由于错误,在使用此运算符时遇到了一些困难Cannot apply indexing with [] to an expression of type ICollection<Book>。尝试使用 an 时出现同样的错误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")
    ));
Run Code Online (Sandbox Code Playgroud)

调用UpdateManyAsync一次而不是调用UpdateOneAsync三次可能很诱人,如上所述。实现如下所示。虽然这有效(调用不会出错),但它不会更新所有三本书。由于它一次访问每个选定的作者,因此它只能对三本书中的两本书执行更新(因为我们特意列出了Guidid与Guid 相同的书籍ParentId)。而在上面,所有三个更新都可以完成,因为我们对同一作者额外迭代了一次。

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")
    );
Run Code Online (Sandbox Code Playgroud)

结束语
这对您有什么帮助?您还有什么需要进一步咨询的吗?半周前我开始研究这些异步方法,涉及我的个人项目,所以我希望我所介绍的内容与您相关。

您可能还对批量写入感兴趣,以捆绑您想要对数据库进行的更改,并让 Mongo Db 优化对数据库的调用,以优化性能。在这方面,我会推荐这个关于性能的批量操作的广泛示例。这个例子也可能是相关的。

  • 问题是如何使用UpdateOneAsync,答案应该尽可能短而不是博文 (5认同)
  • 虽然这是一个非常详细的解释 - 它远远超出了所提出的问题,并且使得很难找到实际问题的答案。 (2认同)

crg*_*den 5

您是否尝试过按照文档中显示的方式进行操作?我想对你来说可能是这样的:

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

更新:

\n

根据OP评论,要发送整个对象而不是更新特定属性,请尝试ReplaceOneAsync

\n
\xe2\x8b\xae\nvar filter = Builders<DummyDocument>.Filter.Eq("Id", document.Id);\nawait db.GetCollection<DummyDocument>().ReplaceOneAsync(filter, new DummyDocument { Test = "bla" }, new UpdateOptions { IsUpsert = true });\n
Run Code Online (Sandbox Code Playgroud)\n