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

6

我想仅更新文档中的单个字段。我知道需要使用UpdateOneAsync。当我尝试这样做时,总是会收到MongoDB.Bson.BsonSerializationException:元素名称“Test”无效。

以下代码重现了我的问题(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" }));
    }
}

错误出现在哪一行? - Nate Barbettini
在调用UpdateOneAsync时抛出异常。StackTrace中的最后一个方法是MongoDB.Bson.IO.BsonWriter.WriteName(String name)。 - msallin
2个回答

10

最近,我在自己的项目中尝试实现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类对象。
  • 任何Book对象的ParentId属性始终与封闭父类的Id相同。当将Author对象插入到authors集合中或初始化具有测试数据的数据库时,会在存储库级别上维护此属性。 Guid标识符用作父子类之间的唯一链接,这可能很方便。
  • 当向集合添加新作家时(例如使用Guid.NewGuid()),Author类对象的Id属性会生成。
  • 我们假设要添加到某些作者的Books集合中的书籍对象列表IEnumerable<Book>。我们假设我们想要将这些添加到已经存在的任何书籍后面,而不是替换那里已经有的书籍。还将进行其他修改。
  • 所做的修改旨在显示方法功能,可能并不总是上下文上合理的。
  • 我们使用asyncawait以及Task异步运行这些I/O绑定操作,与同步运行相比,它可以防止执行线程在多个调用同一方法时锁定。例如,声明类方法时,我们使用async Task AddBooksCollection(IEnumerable<Book> bookEntities)而不是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")
            }
        }
    }
};

测试数据:子书籍分类,添加到现有作者
一些书籍具有与其他书籍相同的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, 等等..) 中来实现多个更新。第一个更新与之前相同,第二个更新将作者版本增加到 1.01,从而使它们与此时没有新书的作者区分开来。为了执行更复杂的查询,我们可以扩展在 GroupBy 调用中创建的 Author 对象,以携带其他数据。这在第三个更新中被利用,其中作者的预期寿命取决于作者的姓氏。请注意 if-else 语句,如 (你有香蕉吗?) ? 如果是 : 如果不是

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"));

嵌套文档:更新单个子书对象
要直接更新子文档字段,我们使用Mongo DB 位置操作符。在C#中,它通过输入[-1]来声明,这相当于Mongo DB Shell中的$。截至撰写本文时,似乎驱动程序仅支持在IList类型中使用[-1]。由于错误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的Guid ids的书)。而在上面,由于我们多次迭代同一个作者,所以可以完成所有三个更新。
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")
    );
总结
这些内容对你有帮助吗?是否还有其他需求需要咨询?我半个星期前开始研究这些异步方法,关于我的个人项目,所以我希望我所介绍的内容与你相关。

你可能也会对批量写入感兴趣,将您要对数据库进行的更改捆绑在一起,并使Mongo Db优化对数据库的调用,以实现性能优化。在这方面,我建议您查看这个详尽的性能批量操作示例。这个示例也可能是相关的。


18
如何使用UpdateOneAsync函数?简短回答即可,无需详细说明。 - Sam
1
感谢您的反馈和负评!考虑到这个问题(在我看来)过于模糊,我选择扩大范围(并且有一定相关性),而不是过于具体(并且不相关)。 - Fhyarnir
2
虽然这是一个非常详细的解释,但它远远超出了所问的问题,使得很难找到实际问题的答案。 - Anthony Nichols
这完全偏离了主题,回答了一个完全不同的问题。 - Rhys Bevilaqua

5

你尝试过按照官方文档中展示的方式进行操作吗?我认为对于你来说可能是这样的:

[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 });

是的,我尝试过了,它可以工作。但这不是我想要的。我想传递一个对象。我可以编写反射使其工作,但我猜想,我只是错误地使用了“ObjectUpdateDefinition”。 - msallin
@msallin,我进行了更新-也许这就是你要找的。 - crgolden
谢谢更新。我不想替换整个文档,只是一些字段。现在我最终使用了反射来完成我认为ObjectUpdateDefinition会做的事情。不过,我仍然想知道如何使用ObjectUpdateDefinition。 - msallin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接