web-dev-qa-db-ja.com

mongodbで複数の配列要素を更新する方法

要素の配列を保持するMongoドキュメントがあります。

.handled = XXの配列内のすべてのオブジェクトの.profile属性をリセットしたいと思います。

ドキュメントは次の形式です。

{
    "_id": ObjectId("4d2d8deff4e6c1d71fc29a07"),
    "user_id": "714638ba-2e08-2168-2b99-00002f3d43c0",
    "events": [{
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 10,
            "data": "....."
        } {
            "handled": 1,
            "profile": 20,
            "data": "....."
        }
        ...
    ]
}

だから、私は次のことを試しました:

.update({"events.profile":10},{$set:{"events.$.handled":0}},false,true)

ただし、各ドキュメントのfirst一致した配列要素のみを更新します。 (これが $-位置演算子 の定義された動作です。)

一致した配列要素をall更新するにはどうすればよいですか?

169
LiorH

現時点では、位置演算子を使用して配列内のすべてのアイテムを更新することはできません。 JIRAを参照してください http://jira.mongodb.org/browse/SERVER-124

回避策として次のことができます。

  • 各アイテムを個別に更新します(events.0.handled events.1.handled ...)または...
  • ドキュメントを読んで、手動で編集を行い、古いドキュメントを置き換えて保存します(アトミックな更新を確認する場合は 「現在の場合更新」 をチェックします)
109
Javier Ferrero

私のために働いたのはこれでした:

db.collection.find({ _id: ObjectId('4d2d8deff4e6c1d71fc29a07') })
  .forEach(function (doc) {
    doc.events.forEach(function (event) {
      if (event.profile === 10) {
        event.handled=0;
      }
    });
    db.collection.save(doc);
  });

Mongoの初心者や、JQueryや友人に精通している人にとっては、より明確だと思います。

63
Daniel Cerecedo

MongoDB 3.6のリリース (およびMongoDB 3.5.12の開発ブランチで利用可能)を使用すると、1つのリクエストで複数の配列要素を更新できるようになりました。

これは、このバージョンで導入された フィルタリングされた位置$[<identifier>] 更新演算子構文を使用します。

db.collection.update(
  { "events.profile":10 },
  { "$set": { "events.$[elem].handled": 0 } },
  { "arrayFilters": [{ "elem.profile": 10 }], "multi": true }
)

.update() またはさらに .updateOne().updateMany().findOneAndUpdate() または .bulkWrite())のオプションに渡される"arrayFilters" methodは、updateステートメントで指定された識別子と一致する条件を指定します。指定された条件に一致する要素が更新されます。

質問の文脈で与えられた"multi"は、これが「複数の要素を更新する」という期待で使用されていましたが、そうではなかったし、そうではないことに注意してください。ここでの使用方法は、「複数のドキュメント」に適用されます。常にそうであるように、または現在は .updateMany() の必須設定として指定されています。最新のAPIバージョン。

NOTEやや皮肉なことに、これは.update()などのメソッドの「options」引数で指定されているため、構文は一般にすべての最近のリリースドライバと互換性があります。バージョン。

ただし、これはmongoシェルには当てはまりません。メソッドがそこに実装される方法(「皮肉にも下位互換性のため」)で、arrayFilters引数は認識されず、オプションを解析する内部メソッドによって削除されます以前のバージョンのMongoDBサーバーと「後方互換性」および「レガシー」.update() API呼び出し構文を提供するため。

したがって、mongoシェルまたはその他の「シェルベース」製品(特にRobo 3T)でコマンドを使用する場合は、3.6以降の開発ブランチまたは製品リリースの最新バージョンが必要です。

positional all $[] も参照してください。これは、「複数の配列要素」も更新しますが、指定された条件に適用せず、配列内のall要素に適用しますそれが望ましいアクションです。

「配列が他の配列内にある」「ネストされた」配列構造にこれらの新しい位置演算子を適用する方法については、 MongoDBによるネストされた配列の更新 も参照してください。

重要-以前のバージョンからアップグレードされたインストールでは、MongoDB機能が有効になっていない可能性があり、ステートメントが失敗する可能性もあります。アップグレード手順がインデックスアップグレードなどの詳細で完了していることを確認してから実行する必要があります。

   db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

または、インストールされているバージョンに適用される上位バージョン。つまり、現在バージョン4以降の"4.0"です。これにより、新しい位置更新演算子などの機能が有効になりました。以下を確認することもできます。

   db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

現在の設定に戻すには

53
Neil Lunn

これは、更新されていないサブドキュメントがまだ残っているドキュメントがあるかどうかを確認するwhileループでも実現できます。この方法では、更新のアトミック性が保持されます(他の多くのソリューションでは保持されません)。

var query = {
    events: {
        $elemMatch: {
            profile: 10,
            handled: { $ne: 0 }
        }
    }
};

while (db.yourCollection.find(query).count() > 0) {
    db.yourCollection.update(
        query,
        { $set: { "events.$.handled": 0 } },
        { multi: true }
    );
}

ループが実行される回数は、profileが10でhandledが0でないサブドキュメントがコレクション内のドキュメントのいずれかで発生する最大回数に等しくなります。コレクションに100個のドキュメントがあり、そのうちの1つにqueryに一致する3つのサブドキュメントがあり、他のすべてのドキュメントに一致するサブドキュメントが少ない場合、ループは3回実行されます。

この方法は、このスクリプトの実行中に別のプロセスによって更新される可能性のある他のデータを破壊する危険を回避します。また、クライアントとサーバー間で転送されるデータの量を最小限に抑えます。

18
Sean

これは、実際には http://jira.mongodb.org/browse/SERVER-124 の長年の問題に関連しています。実際には、「すべてをサポートする明確な構文に対する多くの課題があります。複数の配列の一致が見つかった場合。 一括操作 など、この問題の解決策を「支援する」方法が既に存在します。これらの方法は、この最初の投稿の後に実装されています。

単一の更新ステートメントで単一の一致した配列要素以上を更新することはまだできないため、「マルチ」更新を使用しても、その単一のドキュメントごとに配列内の1つの数学要素のみを更新できます。ステートメント。

現時点で可能な最善の解決策は、一致するすべてのドキュメントを検索してループし、バルク更新を処理することです。これにより、少なくとも多くの操作を単一の応答で単一の応答で送信できます。オプションで .aggregate() を使用して、検索結果で返される配列の内容を、更新選択の条件に一致するものだけに減らすことができます。

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$setDifference": [
               { "$map": {
                   "input": "$events",
                   "as": "event",
                   "in": {
                       "$cond": [
                           { "$eq": [ "$$event.handled", 1 ] },
                           "$$el",
                           false
                       ]
                   }
               }},
               [false]
            ]
        }
    }}
]).forEach(function(doc) {
    doc.events.forEach(function(event) {
        bulk.find({ "_id": doc._id, "events.handled": 1  }).updateOne({
            "$set": { "events.$.handled": 0 }
        });
        count++;

        if ( count % 1000 == 0 ) {
            bulk.execute();
            bulk = db.collection.initializeOrderedBulkOp();
        }
    });
});

if ( count % 1000 != 0 )
    bulk.execute();

配列の「固有の」識別子がある場合、または各要素のすべてのコンテンツが「固有の」要素自体を形成する場合、そこの.aggregate()部分が機能します。これは、 $setDifference の「set」演算子が原因で、配列の一致を処理するために使用される $map 操作から返されるfalse値をフィルタリングするためです。

配列の内容に一意の要素がない場合は、 $redact を使用して別のアプローチを試すことができます。

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$redact": {
        "$cond": {
            "if": {
                "$eq": [ { "$ifNull": [ "$handled", 1 ] }, 1 ]
            },
            "then": "$$DESCEND",
            "else": "$$Prune"
        }
    }}
])

制限事項は、「処理」が実際に他のドキュメントレベルに存在するフィールドである場合、予期しない結果が得られる可能性が高いことですが、そのフィールドが1つのドキュメント位置にのみ表示され、一致する場合は問題ありません。

執筆時点での将来のリリース(3.1 MongoDB以降)には、より簡単な$filter操作が含まれます。

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$project": {
        "events": {
            "$filter": {
                "input": "$events",
                "as": "event",
                "cond": { "$eq": [ "$$event.handled", 1 ] }
            }
        }
    }}
])

また、.aggregate()をサポートするすべてのリリースでは、 $unwind で以下のアプローチを使用できますが、その演算子を使用すると、パイプラインでの配列の拡張により、最も効率の悪いアプローチになります。

db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "events": { "$Push": "$events" }
    }}        
])

MongoDBバージョンが集計出力からの「カーソル」をサポートするすべての場合、これはアプローチを選択し、バルク更新ステートメントを処理するために示された同じコードブロックで結果を反復するだけの問題です。一括操作と集約出力からの「カーソル」は同じバージョン(MongoDB 2.6)で導入されているため、通常は処理のために連携して動作します。

以前のバージョンでさえ、おそらく.find()を使用してカーソルを返し、.update()の繰り返しで配列要素が一致する回数だけステートメントの実行を除外するのがおそらく最善です。

db.collection.find({ "events.handled": 1 }).forEach(function(doc){ 
    doc.events.filter(function(event){ return event.handled == 1 }).forEach(function(event){
        db.collection.update({ "_id": doc._id },{ "$set": { "events.$.handled": 0 }});
    });
});

「マルチ」更新を実行するか、一致するドキュメントごとに複数の更新を処理するより最終的に効率的であると絶対に判断した場合、可能な配列一致の最大数を常に決定し、その数だけ「マルチ」更新を実行できます基本的に、更新するドキュメントがなくなるまで。

MongoDB 2.4および2.2バージョンの有効なアプローチでは、.aggregate()を使用してこの値を見つけることもできます。

var result = db.collection.aggregate([
    { "$match": { "events.handled": 1 } },
    { "$unwind": "$events" },
    { "$match": { "events.handled": 1 } },
    { "$group": {
        "_id": "$_id",
        "count": { "$sum": 1 }
    }},
    { "$group": {
        "_id": null,
        "count": { "$max": "$count" }
    }}
]);

var max = result.result[0].count;

while ( max-- ) {
    db.collection.update({ "events.handled": 1},{ "$set": { "events.$.handled": 0 }},{ "multi": true })
}

いずれにせよ、アップデート内でやりたいことnotがあります:

  1. 配列を「ワンショット」更新しないでください:コード内の配列全体を更新してから$setを更新する方が効率的であると思われる場合各ドキュメントの配列全体。これは処理が高速に思えるかもしれませんが、読み取られて更新が実行されてからアレイの内容が変更されていないという保証はありません。 $setは依然としてアトミック演算子ですが、配列を正しいデータであると「考える」だけで更新するため、読み取りと書き込みの間に発生した変更を上書きする可能性があります。

  2. 更新するインデックス値を計算しない:「ワンショット」アプローチに似ている場合、その位置0と位置2(など)を計算します。これらを更新およびコーディングする要素は次のとおりです。

    { "$set": {
        "events.0.handled": 0,
        "events.2.handled": 0
    }}
    

    繰り返しますが、ここでの問題は、ドキュメントが読み取られたときに見つかったインデックス値が、更新時に配列内の同じインデックス値であるという「推定」です。順序を変更する方法で新しいアイテムが配列に追加されると、それらの位置は有効ではなくなり、間違ったアイテムが実際に更新されます。

したがって、複数の一致した配列要素を単一の更新ステートメントで処理するための合理的な構文が決定されるまで、基本的なアプローチは、個々のステートメントで一致した各配列要素を更新するか(理想的にはBulkで)、または基本的に最大配列要素を計算することです変更された結果が返されなくなるまで更新または更新を続ける。とにかく、ステートメントごとに1つの要素のみを更新する場合でも、一致する配列要素の positional $ 更新を「常に」処理する必要があります。

実際、一括操作は、「複数の操作」になるように動作する操作を処理するための「一般化された」ソリューションであり、複数の配列要素を同じ値で更新するだけでなく、このためのアプリケーションがあるため、もちろん実装されています既に、この問題を解決するための現在の最良のアプローチです。

13
Blakes Seven

私はこれがまだmongoで扱われていないことに驚いています。全体的なmongoは、サブ配列を扱う際に素晴らしいとは思えません。たとえば、サブアレイを数えることはできません。

Javierの最初のソリューションを使用しました。配列をイベントに読み込み、ループしてセットexpを作成します。

var set = {}, i, l;
for(i=0,l=events.length;i<l;i++) {
  if(events[i].profile == 10) {
    set['events.' + i + '.handled'] = 0;
  }
}

.update(objId, {$set:set});

これは、条件テストのコールバックを使用して、関数に抽象化できます

8
lukenofurther

私はC#3.6の最新のドライバーを使用してこれに対する解決策を探していましたが、最終的に解決した修正を次に示します。ここで重要なのは、 "$ []"を使用することです。これは、MongoDBによると、バージョン3.6の時点で新しいものです。 https://docs.mongodb.com/manual/reference/operator/update/positional-all/#up。S[] を参照してください詳細については。

コードは次のとおりです。

{
   var filter = Builders<Scene>.Filter.Where(i => i.ID != null);
   var update = Builders<Scene>.Update.Unset("area.$[].discoveredBy");
   var result = collection.UpdateMany(filter, update, new UpdateOptions { IsUpsert = true});
}

詳細については、元の投稿を参照してください: MongoDB C#ドライバーを使用してすべてのドキュメントから配列要素を削除

4
C0d3 0n3

スレッドは非常に古いですが、ここで答えを探しに来たので、新しいソリューションを提供します。

MongoDBバージョン3.6+では、位置演算子を使用して配列内のすべてのアイテムを更新できるようになりました。 公式ドキュメントはこちら を参照してください。

次のクエリは、ここで尋ねられた質問に対して機能します。また、Java-MongoDBドライバーで検証しましたが、正常に動作します。

.update(   // or updateMany directly, removing the flag for 'multi'
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},  // notice the empty brackets after '$' opearor
   false,
   true
)

これが私のような人を助けることを願っています。

1
ersnh

以下を試してみましたが、うまくいきました。

.update({'events.profile': 10}, { '$set': {'events.$.handled': 0 }},{ safe: true, multi:true }, callback function);

// nodejsの場合のコールバック関数

1
Pranay Saha

このスレッドでは、$ []の使用が間違っていることを示唆する回答がいくつかあることに注意してください。

db.collection.update(
   {"events.profile":10},
   {$set:{"events.$[].handled":0}},
   {multi:true}
)

上記のコードは、「profile」値に関係なく、「events」配列内のすべての要素の「handled」を0に更新します。クエリ{"events.profile":10}は、配列内のドキュメントではなく、ドキュメント全体をフィルタリングするためのものです。この状況では、$[elem]arrayFiltersとともに使用して配列項目の条件を指定し、Neil Lunnの答えが正しいようにする必要があります。

0
Wenda Hu

$ []演算子は、すべてのネストされた配列を選択します。すべての配列項目を '$ []'で更新できます。

.update({"events.profile":10},{$set:{"events.$[].handled":0}},false,true)

参照

0
Beyaz

実際、保存コマンドはDocumentクラスのインスタンスにのみあります。それには多くのメソッドと属性があります。だからあなたは使うことができます リーン() 作業負荷を軽減する機能。こちらをご覧ください。 https://hashnode.com/post/why-are-mongoose-mongodb-odm-lean-queries-faster-than-normal-queries-cillvawhq0062kj53asxoyn7j

セーブ機能に関する別の問題は、マルチセーブと同時に競合データを作成します。 Model.Update 一貫してデータを作成します。したがって、ドキュメントの配列内の複数のアイテムを更新します。おなじみのプログラミング言語を使用して、このようなものを試してください、私はその中にマングースを使用します:

User.findOne({'_id': '4d2d8deff4e6c1d71fc29a07'}).lean().exec()
  .then(usr =>{
    if(!usr)  return
    usr.events.forEach( e => {
      if(e && e.profile==10 ) e.handled = 0
    })
    User.findOneAndUpdate(
      {'_id': '4d2d8deff4e6c1d71fc29a07'},
      {$set: {events: usr.events}},
      {new: true}
    ).lean().exec().then(updatedUsr => console.log(updatedUsr))
})
0
user3176403