Mongooseでドキュメントを更新/アップサートするにはどうすればよいですか?


367

多分それは時間です、おそらくそれは私がまばらなドキュメンテーションに溺れていて、Mongooseでの更新の概念に頭を抱えることができないことです:)

ここに取り引きがあります:

連絡先のスキーマとモデル(短縮されたプロパティ)があります。

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var mongooseTypes = require("mongoose-types"),
    useTimestamps = mongooseTypes.useTimestamps;


var ContactSchema = new Schema({
    phone: {
        type: String,
        index: {
            unique: true,
            dropDups: true
        }
    },
    status: {
        type: String,
        lowercase: true,
        trim: true,
        default: 'on'
    }
});
ContactSchema.plugin(useTimestamps);
var Contact = mongoose.model('Contact', ContactSchema);

必要なフィールドを含むクライアントからのリクエストを受け取り、モデルを使用します。

mongoose.connect(connectionString);
var contact = new Contact({
    phone: request.phone,
    status: request.status
});

そして今、私たちは問題に到達します:

  1. 私は呼び出す場合contact.save(function(err){...})、同じ電話番号と連絡先が既に存在する場合、私はエラーを受け取ります(期待通り-ユニーク)
  2. update()そのメソッドはドキュメントに存在しないため、連絡先を呼び出すことはできません
  3. モデルに対してupdateを呼び出すと
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    、Mongooseのupdate実装は2番目のパラメーターとしてオブジェクトを明らかに必要としないため、ある種の無限ループに入ります。
  4. 私は同じことを行うが、2番目のパラメータで、私は、要求のプロパティの連想配列を渡した場合{status: request.status, phone: request.phone ...}、それは動作しますが-が、その後、私は、特定の連絡先への参照を持っていないし、その見つけることができませんcreatedAtし、updatedAtプロパティを。

つまり、結局のところ、私が試した結果、ドキュメントが与えられたcontact場合、それが存在する場合はどのように更新し、存在しない場合は追加するのでしょうか。

御時間ありがとうございます。


のフックについてpresaveどうですか?
Shamoon

回答:


427

MongooseはこれをfindOneAndUpdateでネイティブにサポートするようになりました(MongoDB findAndModifyを呼び出します)。

upsert = trueオプションは、オブジェクトが存在しない場合にオブジェクトを作成します。デフォルトはfalseです。

var query = {'username': req.user.username};
req.newData.username = req.user.username;

MyModel.findOneAndUpdate(query, req.newData, {upsert: true}, function(err, doc) {
    if (err) return res.send(500, {error: err});
    return res.send('Succesfully saved.');
});

古いバージョンでは、Mongooseはこのメソッドでこれらのフックをサポートしていません:

  • デフォルト
  • セッター
  • バリデーター
  • ミドルウェア

17
これが最新の答えになるはずです。他のほとんどの場合、2つの呼び出しを使用するか、ネイティブのmongodbドライバーにフォールバックします。
ハギー、2014

10
findOneAndUpdateの問題は、事前保存が実行されないことです。
a77icu5 2014年

2
MongooseまたはMongoDBのバグのように聞こえますか?
パスカリウス2014年

8
ドキュメントから:「findAndModifyヘルパーを使用した場合...、以下が適用されない:デフォルト、セッター、バリ、ミドルウェア」mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
ケレン

2
@JamieHutberこれはデフォルトでは設定されていません。これはカスタムプロパティです
Pascalius

194

私は同じ問題を解決するために、3時間しっかり燃やしました。具体的には、ドキュメント全体が存在する場合はそれを「置換」するか、そうでない場合は挿入したかったのです。これが解決策です:

var contact = new Contact({
  phone: request.phone,
  status: request.status
});

// Convert the Model instance to a simple object using Model's 'toObject' function
// to prevent weirdness like infinite looping...
var upsertData = contact.toObject();

// Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error
delete upsertData._id;

// Do the upsert, which works like this: If no Contact document exists with 
// _id = contact.id, then create a new doc using upsertData.
// Otherwise, update the existing doc with upsertData
Contact.update({_id: contact.id}, upsertData, {upsert: true}, function(err{...});

これに関する情報をドキュメントに追加するように要求する問題をMongooseプロジェクトページで作成しまし


1
現時点ではドキュメントは貧弱なようです。。そこページの「更新」のためのAPIドキュメント内のいくつかは、(検索では、次のようになりますが次のとおりです。MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);そしてMyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
CpILL

ケースドキュメントが見つからない場合、どの_idが使用されますか?マングースはそれを生成するのか、それとも照会されたのか?
ハイダー

91

あなたはと親しかった

Contact.update({phone:request.phone}, contact, {upsert: true}, function(err){...})

ただし、2番目のパラメータは、たとえば変更演算子を含むオブジェクトにする必要があります

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})

15
私が{$set: ... }読んでいる自動フォームとしてここに必要な部分はないと思います
CpILL

5
そう、マングースはすべてを$ setに変えると言っています
grantwparks

1
これは執筆時点では有効でした
。MongoDBを

5
ただし、ネイティブドライバーをときどき使用する場合は、$ setを使用しないことをお勧めします。
UpTheCreek 14年

$ setと$ setOnInsertを使用して、挿入の場合に特定のフィールドのみを設定できます
justin.m.chase

73

ええと、私は十分に長く待っていて、答えはありませんでした。最後に、更新/アップサートのアプローチ全体をあきらめて、

ContactSchema.findOne({phone: request.phone}, function(err, contact) {
    if(!err) {
        if(!contact) {
            contact = new ContactSchema();
            contact.phone = request.phone;
        }
        contact.status = request.status;
        contact.save(function(err) {
            if(!err) {
                console.log("contact " + contact.phone + " created at " + contact.createdAt + " updated at " + contact.updatedAt);
            }
            else {
                console.log("Error: could not save contact " + contact.phone);
            }
        });
    }
});

うまくいきますか?うん。これで満足ですか?おそらく違います。1つではなく2つのDB呼び出し。
うまくいけば、将来のMongooseの実装がModel.upsert関数を考え出すでしょう。


2
この例では、MongoDB 2.2で追加されたインターフェースを使用して、ドキュメントフォームでマルチオプションとアップサートオプションを指定します。.. include :: /includes/fact-upsert-multi-options.rstドキュメントにはこれが記載されていますが、ここからどこに行くべきかわかりません。
ドナルドデレク

1
これは機能するはずですが、1つだけ(upsert)が必要なときに2つの操作(find、update)を実行しています。@chrixianはこれを行う正しい方法を示しています。
respectTheCode

12
これは、Mongooseのバリデーターが起動できる唯一の回答であることは注目に値します。docsによると、 updateを呼び出しても検証は行われません。
トムスペンサー、2014年

@fiznoolはrunValidators: true、更新中に手動でオプションを渡すことができるように見えます:更新ドキュメント(ただし、更新バリデーターは実行$set$unset操作のみ)
Danny

.upsert()すべてのモデルで利用できるようにする必要がある場合は、これに基づいて私の答えを参照してください。stackoverflow.com/a/50208331/1586406
spondbob 2018年

24

Promiseのチェーンを使用して達成できる非常にエレガントなソリューション:

app.put('url', (req, res) => {

    const modelId = req.body.model_id;
    const newName = req.body.name;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, {name: newName});
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});

なぜこれは賛成されなかったのですか?素晴らしいソリューションでとてもエレガントなようです
MadOgre

素晴らしい解決策は、実際に私が約束へのアプローチ方法を再考させました。
lux

4
さらにエレガントに書き換えることであろう(model) => { return model.save(); }としてmodel => model.save()、また、(err) => { res.send(err); }などerr => res.send(err);)
ジェレミーThille

1
詳細情報はどこで入手できますか?
V0LT3RR4

17

私はマングースのメンテナーです。ドキュメントを更新する最新の方法は、Model.updateOne()関数を使用することです。

await Contact.updateOne({
    phone: request.phone
}, { status: request.status }, { upsert: true });

アップサートされたドキュメントが必要な場合は、 Model.findOneAndUpdate()

const doc = await Contact.findOneAndUpdate({
    phone: request.phone
}, { status: request.status }, { upsert: true });

重要な点は、filterパラメーターの一意のプロパティをupdateOne()またはfindOneAndUpdate()に配置し、その他のプロパティをパラメーターに配置する必要があることですupdate

ここにチュートリアルがあります Mongoose使用したドキュメントの更新に関するです。


15

この質問に答えるために、StackOverflowアカウントを作成しました。無益にインターウェブを検索した後、私は自分で何かを書いただけです。これは私がどのようにそれをしたので、どんなマングースモデルにも適用できます。この関数をインポートするか、更新を行うコードに直接追加します。

function upsertObject (src, dest) {

  function recursiveFunc (src, dest) {
    _.forOwn(src, function (value, key) {
      if(_.isObject(value) && _.keys(value).length !== 0) {
        dest[key] = dest[key] || {};
        recursiveFunc(src[key], dest[key])
      } else if (_.isArray(src) && !_.isObject(src[key])) {
          dest.set(key, value);
      } else {
        dest[key] = value;
      }
    });
  }

  recursiveFunc(src, dest);

  return dest;
}

次に、マングースドキュメントをアップサートするには、次のようにします。

YourModel.upsert = function (id, newData, callBack) {
  this.findById(id, function (err, oldData) {
    if(err) {
      callBack(err);
    } else {
      upsertObject(newData, oldData).save(callBack);
    }
  });
};

このソリューションでは2つのDB呼び出しが必要になる場合がありますが、次の利点があります。

  • .save()を使用しているため、モデルに対するスキーマ検証
  • update呼び出しで手動で列挙することなく、深くネストされたオブジェクトをupsertできるため、モデルが変更されても、コードの更新について心配する必要はありません

ソースに既存の値がある場合でも、宛先オブジェクトは常にソースをオーバーライドすることに注意してください

また、配列の場合、既存のオブジェクトの配列がそれを置き換えるものよりも長い場合、古い配列の最後の値が残ります。配列全体をアップサートする簡単な方法は、それが目的の場合は、アップサートの前に古い配列を空の配列に設定することです。

更新-2016年1月16日プリミティブ値の配列がある場合の追加条件を追加しました。Mongooseは、「set」関数を使用しないと配列が更新されることを認識しません。


2
これのためにaccを作成するための+1:P .save()を使用するためだけに別の+ 1を与えることができるとよいのです。ありがとう、私もこれをチェックします
user1576978

申し訳ありませんが、ここでは機能しませんでした:(コールスタックサイズを超えました
user1576978

lodashのどのバージョンを使用していますか?lodashバージョン2.4.1を使用しています。ありがとうございます。
アーロンマスト

また、あなたはどのくらい複雑なオブジェクトをアップサートしていますか?それらが大きすぎる場合、ノードプロセスはオブジェクトのマージに必要な再帰呼び出しの数を処理できない場合があります。
アーロンマスト2015

これを使用if(_.isObject(value) && _.keys(value).length !== 0) {しましたが、スタックオーバーフローを停止するためにガード条件を追加する必要がありました。Lodash 4+、ここでは、非オブジェクト値をkeys呼び出しでオブジェクトに変換するようですので、再帰的ガードは常に真でした。たぶんもっと良い方法があるかもしれませんが、それは今私のためにほぼ機能しています...
リチャードG

12

ドキュメントを1つのコレクションに更新またはアップサートする必要があり、次のような新しいオブジェクトリテラルを作成しました。

notificationObject = {
    user_id: user.user_id,
    feed: {
        feed_id: feed.feed_id,
        channel_id: feed.channel_id,
        feed_title: ''
    }
};

データベースの他の場所から取得したデータから構成され、モデルでupdateを呼び出す

Notification.update(notificationObject, notificationObject, {upsert: true}, function(err, num, n){
    if(err){
        throw err;
    }
    console.log(num, n);
});

これは、スクリプトを初めて実行した後に得られる出力です。

1 { updatedExisting: false,
    upserted: 5289267a861b659b6a00c638,
    n: 1,
    connectionId: 11,
    err: null,
    ok: 1 }

そして、これはスクリプトを2回目に実行したときの出力です。

1 { updatedExisting: true, n: 1, connectionId: 18, err: null, ok: 1 }

Mongooseバージョン3.6.16を使用しています


10
app.put('url', function(req, res) {

        // use our bear model to find the bear we want
        Bear.findById(req.params.bear_id, function(err, bear) {

            if (err)
                res.send(err);

            bear.name = req.body.name;  // update the bears info

            // save the bear
            bear.save(function(err) {
                if (err)
                    res.send(err);

                res.json({ message: 'Bear updated!' });
            });

        });
    });

ここでは、マングースの更新方法を解決するためのより良いアプローチがあります。詳細については、Scotch.ioを確認してください。これは間違いなく私のために働きました!!!


5
これがMongoDBの更新と同じことだと考えるのは誤りです。それはアトミックではありません。
Valentin Waeselynck、2015年

1
@ValentinWaeselynckの回答をバックアップしたいと思います。Scotchのコードはクリーンですが、ドキュメントをフェッチしてから更新しています。そのプロセスの途中で、ドキュメントが変更される可能性があります。
Nick Pineda

8

2.6で導入されたバグがあり、2.7にも影響します

アップサートは2.4で正常に機能していました

https://groups.google.com/forum/#!topic/mongodb-user/UcKvx4p4hnY https://jira.mongodb.org/browse/SERVER-13843

見てください、いくつかの重要な情報が含まれています

更新しました:

アップサートが機能しないという意味ではありません。これは、それを使用する方法の良い例です:

User.findByIdAndUpdate(userId, {online: true, $setOnInsert: {username: username, friends: []}}, {upsert: true})
    .populate('friends')
    .exec(function (err, user) {
        if (err) throw err;
        console.log(user);

        // Emit load event

        socket.emit('load', user);
    });

7

これでレコードを更新し、応答として更新されたデータを取得するだけです。

router.patch('/:id', (req, res, next) => {
    const id = req.params.id;
    Product.findByIdAndUpdate(id, req.body, {
            new: true
        },
        function(err, model) {
            if (!err) {
                res.status(201).json({
                    data: model
                });
            } else {
                res.status(500).json({
                    message: "not found any relative data"
                })
            }
        });
});

6

これでうまくいきました。

app.put('/student/:id', (req, res) => {
    Student.findByIdAndUpdate(req.params.id, req.body, (err, user) => {
        if (err) {
            return res
                .status(500)
                .send({error: "unsuccessful"})
        };
        res.send({success: "success"});
    });

});


ありがとう。これがついに私のために働いたものでした!
Luis Febro、

4

ミドルウェアとバリデーターを呼び出しながら作成/更新する最も簡単な方法は次のとおりです。

Contact.findOne({ phone: request.phone }, (err, doc) => {
    const contact = (doc) ? doc.set(request) : new Contact(request);

    contact.save((saveErr, savedContact) => {
        if (saveErr) throw saveErr;
        console.log(savedContact);
    });
})

3

ここに到着し、フックのサポートによる「アップサーティング」のための良い解決策を探している人にとって、これは私がテストして作業したものです。それでも2つのDB呼び出しが必要ですが、単一の呼び出しで試したものよりもはるかに安定しています。

// Create or update a Person by unique email.
// @param person - a new or existing Person
function savePerson(person, done) {
  var fieldsToUpdate = ['name', 'phone', 'address'];

  Person.findOne({
    email: person.email
  }, function(err, toUpdate) {
    if (err) {
      done(err);
    }

    if (toUpdate) {
      // Mongoose object have extra properties, we can either omit those props
      // or specify which ones we want to update.  I chose to update the ones I know exist
      // to avoid breaking things if Mongoose objects change in the future.
      _.merge(toUpdate, _.pick(person, fieldsToUpdate));
    } else {      
      toUpdate = person;
    }

    toUpdate.save(function(err, updated, numberAffected) {
      if (err) {
        done(err);
      }

      done(null, updated, numberAffected);
    });
  });
}

3

ジェネレーターが利用可能な場合は、さらに簡単になります。

var query = {'username':this.req.user.username};
this.req.newData.username = this.req.user.username;
this.body = yield MyModel.findOneAndUpdate(query, this.req.newData).exec();

3

他の解決策は私にとってうまくいきませんでした。私は投稿リクエストを使用しており、他に挿入されている場合はデータを更新しています。また、削除する必要があるリクエスト本文とともに_idが送信されます。

router.post('/user/createOrUpdate', function(req,res){
    var request_data = req.body;
    var userModel = new User(request_data);
    var upsertData = userModel.toObject();
    delete upsertData._id;

    var currentUserId;
    if (request_data._id || request_data._id !== '') {
        currentUserId = new mongoose.mongo.ObjectId(request_data._id);
    } else {
        currentUserId = new mongoose.mongo.ObjectId();
    }

    User.update({_id: currentUserId}, upsertData, {upsert: true},
        function (err) {
            if (err) throw err;
        }
    );
    res.redirect('/home');

});

2
//Here is my code to it... work like ninj

router.param('contractor', function(req, res, next, id) {
  var query = Contractors.findById(id);

  query.exec(function (err, contractor){
    if (err) { return next(err); }
    if (!contractor) { return next(new Error("can't find contractor")); }

    req.contractor = contractor;
    return next();
  });
});

router.get('/contractors/:contractor/save', function(req, res, next) {

    contractor = req.contractor ;
    contractor.update({'_id':contractor._id},{upsert: true},function(err,contractor){
       if(err){ 
            res.json(err);
            return next(); 
            }
    return res.json(contractor); 
  });
});


--

2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});

このコードスニペットは問題を解決する可能性がありますが、なぜまたはどのように質問に答えるかは説明していません。コードの説明を含めてください。これ投稿の品質を向上させるのに役立ちます。あなたは将来の読者のための質問に答えていることを覚えておいてください、そしてそれらの人々はあなたのコード提案の理由を知らないかもしれません。 報告者/レビュアー: このようなコードのみの回答の場合は、投票しないでください。削除しないでください。
Patrick

2

以下の技術ガイ旅行の答え、すでに素晴らしいが、私たちはプラグインを作成し、それはので、私たちはそれを初期化後、マングースにそれを添付することができ .upsert()、すべてのモデルで利用できるようになります。

plugins.js

export default (schema, options) => {
  schema.statics.upsert = async function(query, data) {
    let record = await this.findOne(query)
    if (!record) {
      record = new this(data)
    } else {
      Object.keys(data).forEach(k => {
        record[k] = data[k]
      })
    }
    return await record.save()
  }
}

db.js

import mongoose from 'mongoose'

import Plugins from './plugins'

mongoose.connect({ ... })
mongoose.plugin(Plugins)

export default mongoose

その後User.upsert({ _id: 1 }, { foo: 'bar' })YouModel.upsert({ bar: 'foo' }, { value: 1 })いつでも好きなときに好きなことができます。


2

しばらくしてこの問題に戻り、アーロンマストの回答に基づいてプラグインを公開することにしました。

https://www.npmjs.com/package/mongoose-recursive-upsert

mongooseプラグインとして使用します。渡されたオブジェクトを再帰的にマージする静的メソッドを設定します。

Model.upsert({unique: 'value'}, updateObject});

0

このコーヒースクリプトはノードで機能します-トリックは、クライアントから送信および返されるときに_idがObjectIDラッパーを取り除くため、更新のためにこれを置き換える必要があります(_idが指定されていない場合、保存は挿入と追加に戻ります) 1)。

app.post '/new', (req, res) ->
    # post data becomes .query
    data = req.query
    coll = db.collection 'restos'
    data._id = ObjectID(data._id) if data._id

    coll.save data, {safe:true}, (err, result) ->
        console.log("error: "+err) if err
        return res.send 500, err if err

        console.log(result)
        return res.send 200, JSON.stringify result

0

Martin Kuzdowiczが上に投稿したものに基づいて構築します。私は以下を使用して、mongooseとjsonオブジェクトのディープマージを使用した更新を行います。mongooseのmodel.save()関数に加えて、これはmongooseがjsonの他の値に依存するものでも完全な検証を行うことを可能にします。deepmergeパッケージhttps://www.npmjs.com/package/deepmergeが必要です。しかし、それは非常に軽量なパッケージです。

var merge = require('deepmerge');

app.put('url', (req, res) => {

    const modelId = req.body.model_id;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, merge(model.toObject(), req.body));
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});

1
NoSQLインジェクションをreq.bodyテストする前に、現状のまま使用しないように注意してください(owasp.org/index.php/Testing_for_NoSQL_injectionを参照)。
Traveling Tech Guy

1
@TravelingTechGuy NodeとMongooseを初めて使用する際の注意に感謝します。バリデーターを備えた私のマングースモデルは、注射の試みをキャッチするのに十分ではないでしょうか?model.save()の実行中
Chris Deleo

-5

上記の投稿を読んだ後、私はこのコードを使用することにしました:

    itemModel.findOne({'pid':obj.pid},function(e,r){
        if(r!=null)
        {
             itemModel.update({'pid':obj.pid},obj,{upsert:true},cb);
        }
        else
        {
            var item=new itemModel(obj);
            item.save(cb);
        }
    });

rがnullの場合、新しいアイテムを作成します。それ以外の場合、更新では新しいアイテムが作成されないため、更新でupsertを使用します。


モンゴへの2回の呼び出しなら、それは本当にアップサートではありませんか?
ハギー、2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.