Оновіть поле MongoDB, використовуючи значення іншого поля


372

Чи можливо в MongoDB оновити значення поля, використовуючи значення з іншого поля? Еквівалентний SQL був би на кшталт:

UPDATE Person SET Name = FirstName + ' ' + LastName

І псевдокодом MongoDB було б:

db.person.update( {}, { $set : { name : firstName + ' ' + lastName } );

Відповіді:


258

Найкращий спосіб зробити це у версії 4.2+, яка дозволяє використовувати конвеєрний конвеєр у документі оновлення та updateOne, updateManyабоupdate метод збору. Зауважте, що остання застаріла у більшості, якщо не у всіх драйверах мов.

MongoDB 4.2+

Версія 4.2 також представила $setоператор стадії трубопроводу, який є псевдонімом для $addFields. Я буду використовувати $setтут, як він відображає те, що ми намагаємось досягти.

db.collection.<update method>(
    {},
    [
        {"$set": {"name": { "$concat": ["$firstName", " ", "$lastName"]}}}
    ]
)

MongoDB 3.4+

В 3.4+ ви можете використовувати $addFieldsі $outоператори конвеєрного конвеєра.

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

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

MongoDB 3.2 та 3.0

Ми це робимо, використовуючи $projectнаші документи та використовуючи $concatоператор агрегації рядків, щоб повернути об'єднаний рядок. ми звідти ви потім повторюєте курсор і використовуєте $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

З цієї версії вам потрібно використати застарілий BulkAPI та пов'язані з ним методи .

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

Я думаю, що проблема з кодом для "MongoDB 3.2 або новішої". Оскільки forEach є асинхронізацією, зазвичай нічого не буде записано в останній bulkWrite.
Віктор Хедефальк

3
4.2+ Не працює. MongoError: Поле префіксу долара ($) "$ concat" у "ім'я. $ Concat" недійсне для зберігання.
Джош

@JoshWoodcock, я думаю, у вас був помилковий друк у запиті, який ви виконуєте. Я пропоную вам подвійну перевірку.
стиване

@JoshWoodcock Це прекрасно працює. Перевірте це, використовуючи веб-оболонку MongoDB
стиване

2
Для тих, хто стикається з тією ж проблемою, описаний @JoshWoodcock: зверніть увагу, що відповідь на 4.2+ описує конвеєрний конвеєр , тому не пропускайте квадратні дужки у другому параметрі!
філш

240

Ви повинні повторити. Для вашого конкретного випадку:

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

4
Що станеться, якщо інший користувач змінив документ між вашим find () та вашим save ()?
UpTheCreek

3
Щоправда, але для копіювання між полями не потрібно, щоб транзакції були атомними.
UpTheCreek

3
Важливо зауважити, що save()повністю замінює документ. Слід використовувати update()замість цього.
Карлос

12
Як щодоdb.person.update( { _id: elem._id }, { $set: { name: elem.firstname + ' ' + elem.lastname } } );
Philipp Jardas

1
Я створив функцію під назвою, create_guidяка створювала лише унікальний посібник на документ, коли його повторювали forEachтаким чином (тобто просто використання create_guidв updateоператорі з тим, що mutli=trueвикликало генерування однакових інструкцій для всіх документів). Ця відповідь спрацювала для мене чудово. +1
rmirabelle

103

Мабуть, є спосіб зробити це ефективно, оскільки MongoDB 3.4 див . Відповідь стиване .


Відповідь застаріла нижче

Ви не можете посилатися на сам документ в оновлення (поки). Вам потрібно буде повторити документи та оновити кожен документ за допомогою функції. Див. Цю відповідь для прикладу, або цю для серверної eval().


31
Чи діє це і сьогодні?
Крістіан Енгель

3
@ChristianEngel: Схоже, так. Я не зміг знайти нічого в документах MongoDB, що згадує посилання на поточний документ в updateоперації. Цей запит, пов’язаний із функцією, також не вирішений.
Niels van der Rest

4
Чи все-таки він дійсний у квітні 2017 року? Або вже є нові функції, які можуть це зробити?
Кім

1
@Kim Схоже, він все ще діє. Також запит на функцію, на який @ niels-van-der-rest було вказано ще у 2013 році, все ще є OPEN.
Danziger

8
це вже неправдива відповідь, подивіться на відповідь
@styvane

45

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

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/


2
Що станеться, якщо інший користувач редагував особу між find () та save ()? У мене є випадок, коли на один і той же об’єкт можна робити кілька дзвінків, змінюючи їх, виходячи з їх поточних значень. 2-му користувачеві слід чекати з читанням, поки 1-го не буде зроблено із збереженням. Це досягає цього?
Марко

4
Про 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. посилання
ppython

10

Щодо цієї відповіді , відповідно до цього оновлення функція знімка застаріла у версії 3.6 . Отже, у версії 3.6 і вище, операцію можна виконати таким чином:

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

9

Починаючи Mongo 4.2, db.collection.update()можна прийняти конвеєрний конвеєр, нарешті дозволяючи оновити / створити поле на основі іншого поля:

// { firstName: "Hello", lastName: "World" }
db.collection.update(
  {},
  [{ $set: { name: { $concat: [ "$firstName", " ", "$lastName" ] } } }],
  { multi: true }
)
// { "firstName" : "Hello", "lastName" : "World", "name" : "Hello World" }
  • Перша частина {}- це запит на відповідність, фільтруючи документи, які потрібно оновити (у нашому випадку всі документи).

  • Друга частина [{ $set: { name: { ... } }]- це конвеєрний конвеєр з оновленням (зверніть увагу на квадратні дужки, що означають використання конвеєрного конвеєра). $setє новим оператором агрегації та псевдонімом $addFields.

  • Не забувайте { multi: true }, інакше буде оновлено лише перший відповідний документ.


8

Я спробував вищевказане рішення, але виявив його непридатним для великої кількості даних. Потім я виявив функцію потоку:

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
Чим це відрізняється? Чи буде пригнічена пара за допомогою оновлення? Чи маєте ви на це посилання? Документи Монго досить бідні.
Ніко

2

Ось що ми придумали для копіювання одного поля в інше для ~ 150_000 записів. Це зайняло близько 6 хвилин, але все ще значно менш ресурсомістке, ніж було б для інстанціювання та ітерації над тією ж кількістю рубінових об’єктів.

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)

1

З MongoDB версії 4.2+ , поновлення є більш гнучкими , оскільки це дозволяє використовувати агрегацію трубопроводу в її update, updateOneі updateMany. Тепер ви можете перетворити ваші документи за допомогою операторів агрегації, а потім оновити без необхідності вказати $setкоманду explicity (замість цього ми використовуємо$replaceRoot: {newRoot: "$$ROOT"} )

Тут ми використовуємо сукупний запит для вилучення часової позначки з поля "_id" MongoDB у полі "_id" та оновлення документів (я не є експертом у SQL, але я думаю, що SQL не надає автоматично створеного ObjectID, який має на ньому часову позначку, вам доведеться автоматично створити цю дату)

var collection = "person"

agg_query = [
    {
        "$addFields" : {
            "_last_updated" : {
                "$toDate" : "$_id"
            }
        }
    },
    {
        $replaceRoot: {
            newRoot: "$$ROOT"
        } 
    }
]

db.getCollection(collection).updateMany({}, agg_query, {upsert: true})

Вам не потрібно { $replaceRoot: { newRoot: "$$ROOT" } }; це означає заміну документа само собою, що безглуздо. Якщо ви заміните $addFieldsйого псевдонімом $setі updateManyякий є одним із псевдонімів update, то ви отримаєте точну відповідь, як і цей вище.
Xavier Guihot
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.