Обновите вложенный документ MongoDB, если родительский документ может не существовать

Вот мои данные, состоящие из коллекции books и подколлекции books.reviews.

books = [{
    _id: ObjectId("5558f40ad498c653748cf045"),
    title: "Widget XYZ",
    isbn: 1234567890,
    reviews: [
        {
            _id: ObjectId("5558f40ad498c653748cf046"),
            userId: "01234",
            rating: 5,
            review: "Yay, this great!"
        },
        {
            _id: ObjectId("5558f40ad498c653748cf047"),
            userId: "56789",
            rating: 3,
            review: "Meh, this okay."
        }
    ]
}]

В Node.js (с использованием Mongoose) мне нужно, чтобы пользователи могли отправлять обзоры через форму, отправляя обзор и isbn книги на сервер со следующими условиями:

  1. Если книги с определенным номером isbn еще не существует, добавьте ее, а затем добавьте обзор (очевидно, его еще не существует).
  2. If the book does exist...
    • If the review doesn't exist for this book for this user, add it.
    • Если отзыв для этой книги существует для этого пользователя, обновите его.

Я могу сделать это с помощью 4 отдельных запросов, но я знаю, что это не оптимально. Какое наименьшее количество запросов я мог бы использовать (и, конечно, что это за запросы)?


person Horace    schedule 17.05.2015    source источник
comment
Как вы проверяете, существует ли уже отзыв? Я могу придумать способ сделать это в одном запросе, но он предполагает новый обзор.   -  person Jordan P    schedule 18.05.2015


Ответы (1)


В основном у вас есть 3 случая:

  1. и книга и рецензия существуют. Это просто $set
  2. книга есть, а рецензии нет. Это нужно $push
  3. книги не существует. Это нужно {upsert:1} и $setOnInsert

Мне не удалось найти способ объединить какие-либо два из них без ущерба для целостности данных в случае сбоя (помните, что в MongoDB нет атомарных транзакций).

Итак, моя лучшая идея заключается в следующем:

// Case 1:
db.books.update({isbn:'1234567890',
                 review: { $elemMatch: {userID: '01234'}}},
                {$set: {'review.$.rating': NEW_RATING}}
               )

// Case 2:
db.books.update({isbn:'1234567890',
                 review: { $not: { $elemMatch: {userID: '01234'}}}},
                {$push: {review: {rating: NEW_RATING, userID:'01234'}}}
               )

// Case 3:
db.books.update({isbn:'1234567890'},
                {$setOnInsert: {review: [{rating: NEW_RATING, userID:'01234'}]}},
                {upsert:1}
               )

Вы можете вслепую запустить эти три обновления подряд, так как между ними нет перекрывающихся случаев. Прелесть в том, что все эти операции идемпотентны. Таким образом, вы можете применить их один или несколько раз и всегда получать один и тот же результат. Это особенно важно в случае отказа. Кроме того, ваша БД не может быть несогласованной или потерять существующие данные в случае сбоя. В худшем случае обзор не обновляется. Наконец, это должно гарантировать согласованность данных даже в случае одновременных обновлений (т. е. в этом случае одно обновление перезапишет другое, но у вас не должно быть двух документов для одной и той же книги или двух обзоров). того же пользователя для той же книги).
Этот последний пункт должен быть подтвержден, так как здесь уже поздно, поэтому мой анализ может быть несколько сомнительным.

И последнее замечание: если вы хотите сократить количество циклов обмена между MongoDB и вашим приложением, вы можете взглянуть на update команда базы данных, позволяющая объединить несколько обновлений в одну команду.

person Sylvain Leroux    schedule 18.05.2015
comment
Я потратил 20 минут, пытаясь придумать, как сломать это решение, но оно кажется действительно надежным. Однако что произойдет, если я добавлю {upsert:1} к случаю 2? - person Horace; 19.05.2015
comment
@Horace Если вы добавите {upsert:1} к случаю 2, он больше не будет идемпотентным: повторный ввод одной и той же команды приведет к нескольким вставкам. Это допустимо, если и только если вы предпримете дополнительные меры в случае аварийного переключения. Возможно, просто запустив шаг 2, только если на шаге 1 не было совпадения (кстати, и запустив шаг 3, только если на шаге 2 тоже не было совпадения); это простая логика if ... else if .... else ..., но теперь вы больше подвержены человеческим ошибкам при обслуживании кода. OTOH, это меньше нагрузки на БД. Но вы больше не можете просто отправить массовое обновление, содержащее все три обновления в виде необработанного... - person Sylvain Leroux; 19.05.2015
comment
Спасибо, это отличный ответ. - person Horace; 20.05.2015
comment
Это отлично работает. Я выполняю ваши дела одним обращением к серверу через BookModel.db.db.command({update:'books', ...}), и это очень быстро. Однако я беспокоюсь о том, чтобы обойти принудительное применение схемы Mongoose. - person Horace; 20.05.2015