Обновить поле MongoDB, используя значение другого поля

289

В MongoDB можно ли обновить значение поля, используя значение из другого поля? Эквивалентный SQL будет примерно таким:

UPDATE Person SET Name = FirstName + ' ' + LastName

И псевдокод MongoDB будет:

db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );
  • 5
    Хороший вопрос. Может быть, вам нужно подождать / проголосовать за jira.mongodb.org/browse/SERVER-458
  • 3
    Точный запрос функции - jira.mongodb.org/browse/SERVER-11345 - все еще открыт, еще не сработал.
Показать ещё 2 комментария
Теги:
mongodb-query
aggregation-framework

7 ответов

103
Лучший ответ

Видимо, есть способ сделать это эффективно, начиная с MongoDB 3.4, см. Ответ стиване.


Устаревший ответ ниже

Вы не можете ссылаться на сам документ в обновлении (пока). Вам нужно будет перебирать документы и обновлять каждый документ с помощью функции. Посмотрите этот ответ для примера, или этот для серверной eval().

  • 31
    Это все еще актуально сегодня?
  • 3
    @ChristianEngel: Кажется, так. Я не смог найти ничего в документах MongoDB, в которых упоминается ссылка на текущий документ в операции update . Этот связанный запрос по-прежнему не решен.
Показать ещё 5 комментариев
225

Вы должны перебирать. Для вашего конкретного случая:

db.person.find().snapshot().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);
  • 4
    Что произойдет, если другой пользователь изменил документ между вашим find () и вашим save ()?
  • 0
    Насколько я знаю, MongoDB не безопасен для транзакций
Показать ещё 12 комментариев
116

Лучший способ сделать это - использовать структуру агрегации для вычисления нашего нового поля.

MongoDB 3.4

Наиболее эффективное решение в MongoDB 3.4 с помощью $addFields и $out операторов компоновки агрегации.

db.collection.aggregate(
    [
        { "$addFields": { 
            "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
        }},
        { "$out": "collection" }
    ]
)

Обратите внимание, что этот не обновляет вашу коллекцию, а вместо этого заменяет существующую коллекцию или создает новую. Также для операций обновления, требующих "типа casting" вам потребуется обработка на стороне клиента, и в зависимости от операции вам может понадобиться использовать метод find() вместо метода .aggreate().

MongoDB 3.2 и 3.0

Как мы это делаем, $project в наших документах и ​​используйте $concat, чтобы вернуть конкатенированную строку. we Оттуда вы затем перебираете курсор и используете оператор обновления $set, чтобы добавить новое поле в свои документы, используя массовые операции для максимальной эффективности.

Запрос агрегирования:

var cursor = db.collection.aggregate([ 
    { "$project":  { 
        "name": { "$concat": [ "$firstName", " ", "$lastName" ] } 
    }}
])

MongoDB 3.2 или новее

вам нужно использовать метод bulkWrite.

var requests = [];
cursor.forEach(document => { 
    requests.push( { 
        'updateOne': {
            'filter': { '_id': document._id },
            'update': { '$set': { 'name': document.name } }
        }
    });
    if (requests.length === 500) {
        //Execute per 500 operations and re-init
        db.collection.bulkWrite(requests);
        requests = [];
    }
});

if(requests.length > 0) {
     db.collection.bulkWrite(requests);
}

MongoDB 2.6 и 3.0

В этой версии вам нужно использовать теперь устаревший Bulk API и его связанные методы.

var bulk = db.collection.initializeUnorderedBulkOp();
var count = 0;

cursor.snapshot().forEach(function(document) { 
    bulk.find({ '_id': document._id }).updateOne( {
        '$set': { 'name': document.name }
    });
    count++;
    if(count%500 === 0) {
        // Excecute per 500 operations and re-init
        bulk.execute();
        bulk = db.collection.initializeUnorderedBulkOp();
    }
})

// clean up queues
if(count > 0) {
    bulk.execute();
}

MongoDB 2.4

cursor["result"].forEach(function(document) {
    db.collection.update(
        { "_id": document._id }, 
        { "$set": { "name": document.name } }
    );
})
  • 0
    Отличный ответ. Просто интересно, вызывает ли .length каждая итерация в монго так же медленно, как обычный javascript, где он пересчитывает длину при каждом вызове?
  • 2
    @ notbad.jpeg Я могу сказать, медленно это или нет, но свойство length проверяется на каждой итерации. Это то, что мне нужно будет проверить позже. Другой вариант, если это медленно, это использовать счетчик, который вы затем увеличиваете на 1 на каждой итерации.
Показать ещё 4 комментария
42

Для базы данных с высокой активностью вы можете столкнуться с проблемами, когда ваши обновления влияют на активное изменение записей, и по этой причине я рекомендую использовать снимок()

db.person.find().snapshot().forEach( function (hombre) {
    hombre.name = hombre.firstName + ' ' + hombre.lastName; 
    db.person.save(hombre); 
});

http://docs.mongodb.org/manual/reference/method/cursor.snapshot/

  • 1
    Что произойдет, если другой пользователь отредактировал человека между find () и save ()? У меня есть случай, когда несколько вызовов могут быть сделаны к одному и тому же объекту, меняя их в зависимости от их текущих значений. Второму пользователю следует подождать с чтением, пока первый не завершит сохранение. Это делает это?
  • 3
    О snapshot() : Deprecated in the mongo Shell since v3.2. Starting in v3.2, the $snapshot operator is deprecated in the mongo shell. In the mongo shell, use cursor.snapshot() instead. ссылка на сайт
9

Я попробовал вышеуказанное решение, но я счел его непригодным для больших объемов данных. Затем я обнаружил функцию потока:

MongoClient.connect("...", function(err, db){
    var c = db.collection('yourCollection');
    var s = c.find({/* your query */}).stream();
    s.on('data', function(doc){
        c.update({_id: doc._id}, {$set: {name : doc.firstName + ' ' + doc.lastName}}, function(err, result) { /* result == true? */} }
    });
    s.on('end', function(){
        // stream can end before all your updates do if you have a lot
    })
})
  • 1
    Чем это отличается? Будет ли пар задушен активностью обновления? Есть ли у вас какие-либо ссылки на это? Документы Mongo довольно бедны.
2

Вот что мы придумали для копирования одного поля в другое для ~ 150_000 записей. Это заняло около 6 минут, но по-прежнему значительно менее ресурсоемким, чем это было бы для создания экземпляра и повторения одного и того же количества объектов ruby.

js_query = %({
  $or : [
    {
      'settings.mobile_notifications' : { $exists : false },
      'settings.mobile_admin_notifications' : { $exists : false }
    }
  ]
})

js_for_each = %(function(user) {
  if (!user.settings.hasOwnProperty('mobile_notifications')) {
    user.settings.mobile_notifications = user.settings.email_notifications;
  }
  if (!user.settings.hasOwnProperty('mobile_admin_notifications')) {
    user.settings.mobile_admin_notifications = user.settings.email_admin_notifications;
  }
  db.users.save(user);
})

js = "db.users.find(#{js_query}).forEach(#{js_for_each});"
Mongoid::Sessions.default.command('$eval' => js)
0

Что касается этого ответа, функция моментального снимка устарела в версии 3.6, согласно этому обновлению. Итак, на версии 3.6 и выше, можно выполнить операцию следующим образом:

db.person.find().forEach(
    function (elem) {
        db.person.update(
            {
                _id: elem._id
            },
            {
                $set: {
                    name: elem.firstname + ' ' + elem.lastname
                }
            }
        );
    }
);

Ещё вопросы

Сообщество Overcoder
Наверх
Меню