Драйвер C # MongoDb: Как использовать «UpdateOneAsync» - PullRequest
0 голосов
/ 02 октября 2019

Я хотел бы обновить только одно поле документа. Я имею в виду, чтобы понять, что я должен использовать UpdateOneAsync. Когда я пытаюсь это сделать, я всегда получаю t MongoDB.Bson.BsonSerializationException : Element name 'Test' is not valid'..

Следующий код воспроизводит мою проблему (xUnit, .NET Core, MongoDb в Docker).

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 ]

1 голос
/ 31 октября 2019

Недавно я экспериментировал с внедрением метода 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 некоторых авторов. Мы предполагаем, что хотим добавить их в любые существующие книги, если они есть, вместо замены уже существующих. Также будут сделаны другие модификации.
  • Сделанные модификации предназначены для отображения функциональности метода и не всегда могут быть контекстно-зависимыми.
  • Мы выполняем эти операции ввода-вывода с асинхронной синхронизацией, используя async и await и Task. Это предотвращает блокировку потока выполнения при нескольких вызовах одного и того же метода по сравнению с синхронным выполнением. Например, вместо AddBooksCollection(IEnumerable<Book> bookEntities) мы используем async Task AddBooksCollection(IEnumerable<Book> bookEntities) при объявлении метода класса.

Тестовые данные: классы родительского автора для инициализации базы данных
Вставьте их внаша коллекция с т.е. InsertMany или InsertManyAsync.

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 я здесь показываю оба способа сделать это (выберите один). Мы используем несколько фильтров одновременно, т.е. as 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. На момент написания этого, похоже, что драйвер поддерживает только [-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, которые имели равные Guid ParentId s). Хотя выше все три обновления могут быть завершены, потому что мы перебираем одного и того же автора один раз.

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 для оптимизации обращений к базе данных для оптимизации производительности. В связи с этим я бы порекомендовал этот обширный пример массовых операций для производительности . Этот пример также может быть уместным.

0 голосов
/ 02 октября 2019

Вы пытались сделать это так, как показано в документах ? Я думаю, что для вас это может быть что-то вроде:

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