Mongodbで元のデータを使いUpdateする方法
Mongodbを使っているとある列(プロパティ)の値を元にUpdateをしたい時ってありますよね?
UPDATE table SET fugafuga = fugafuga+3;
SQLだとこんな感じのやつです(SQLの知識があまりないため、間違ってたらごめんなさい)。この場合ですと、Mongodbには$inc
というものがあり、db.collection.update()
を使って更新することができます。
しかし、こんな場合はどうでしょうか?
UPDATE table SET name_size = length(name);
このように、name列の文字列の長さを元に、Updateをしたい。このような複雑な更新をMongodbでしようと思ってドキュメントを探しても、見つかりませんでした。もしかしたら、見逃しているかもしれませんが。
Mongodbのコレクションのデータをもとに、そのコレクションを更新する方法を見つけましたので、紹介したいと思います。
海外の方が解決策を載せてくれてました
色々検索してみると、次のような文章を見つけました。
Update: If all you have to do is change the structure of a document without changing the values, see gipset's answer for a nice solution.
According to a (now unavailable) comment on the Update documentation page, you cannot reference the current document's properties from within an update().
You'll have to iterate through all the documents and update them like this:
db.events.find().snapshot().forEach( function (e) { // update document, using its own properties e.coords = { lat: e.lat, lon: e.lon }; // remove old properties delete e.lat; delete e.lon; // save the updated document db.events.save(e); } )
Such a function can also be used in a map-reduce job or a server-side db.eval() job, depending on your needs.
これをGoogle翻訳で日本語にすると、
更新:値を変更せずにドキュメントの構造を変更するだけで済む場合は、素晴らしい解決策のgipsetの答えをご覧ください。
Update documentationページの(現在利用できない)コメントによれば、update()内から現在のドキュメントのプロパティを参照することはできません。
すべてのドキュメントを繰り返して、次のように更新する必要があります:
db.events.find().snapshot().forEach( function (e) { // update document, using its own properties e.coords = { lat: e.lat, lon: e.lon }; // remove old properties delete e.lat; delete e.lon; // save the updated document db.events.save(e); } )
このような関数は、必要に応じてmap-reduceジョブまたはサーバー側のdb.eval()ジョブでも使用できます。
一般化してみる
先ほどの例は少し特殊な例です。既にある列lat
,lon
からオブジェクトを作り、それを更新したいという内容でした。ついでに元からある二つの列を削除しようという事です。
これを一般化してみます。
SQLだとUPDATE table SET value = value + 200;
とするUpdate文をmongodbでは
db.collection.find().snapshot().forEach( function(e){ e.value = e.value+200; db.collection.save(e); } )
と表せます。つまり、
db.コレクション名.find({更新したいデータの条件指定}).snapshot().forEach( function(e){ //Updateしたい内容 db.コレクション名.save(e); } )
ですね!
データを使って実際に試してみる
こんなデータがあるとします。
> db.coll1.find({},{_id:0}); { "name" : "山田", "value" : 101, "text" : "リンゴ" } { "name" : "田中", "value" : 102, "text" : "バナナ" } { "name" : "加藤", "value" : 103, "text" : "グレープフルーツ" } { "name" : "佐藤", "value" : 104, "text" : "キウイフルーツ" } { "name" : "武田", "value" : 105, "text" : "スイカ" }
バックアップを取る
db.collection.copyTo(newCollection)
でコレクションをコピーできます。
> db.coll1.copyTo("coll1bak") WARNING: db.eval is deprecated 5
中身を確認すると同じものが入っていますね!
> db.coll1bak.find({},{_id:0}) { "name" : "山田", "value" : 101, "text" : "リンゴ" } { "name" : "田中", "value" : 102, "text" : "バナナ" } { "name" : "加藤", "value" : 103, "text" : "グレープフルーツ" } { "name" : "佐藤", "value" : 104, "text" : "キウイフルーツ" } { "name" : "武田", "value" : 105, "text" : "スイカ" }


プロパティを加算
プロパティvalue
の値に100
を加算するUpdateをしてみましょう。
db.coll1.find().snapshot().forEach( function(e){ e.value = e.value+100; db.coll1.save(e); } )
一般化したものから、コードを書くとこんな風になります。
> db.coll1.find().snapshot().forEach( ... function(e){ ... e.value = e.value+100; ... db.coll1.save(e); ... } ... ) > db.coll1.find({},{_id:0}) { "name" : "山田", "value" : 201, "text" : "リンゴ" } { "name" : "田中", "value" : 202, "text" : "バナナ" } { "name" : "加藤", "value" : 203, "text" : "グレープフルーツ" } { "name" : "佐藤", "value" : 204, "text" : "キウイフルーツ" } { "name" : "武田", "value" : 205, "text" : "スイカ" }
実行に成功すると、何も出力しませんでした。その後、find()
で検索すると、確かにvalue
の値が+100
されていることが分かります。
指定した範囲だけUpdate
SQLで言うWhile
句のようなものを使い更新してみましょう。
value>202
のデータに新たにプロパティisGT202
を追加し、値にtrue
を追加してみます。
db.coll1.find({value:{$gt:202}}).snapshot().forEach( function(e){ e.isGT202 = true; db.coll1.save(e); } )
結果は、
> db.coll1.find({},{_id:0}); { "name" : "山田", "value" : 201, "text" : "リンゴ" } { "name" : "田中", "value" : 202, "text" : "バナナ" } { "name" : "加藤", "value" : 203, "text" : "グレープフルーツ", "isGT202" : true } { "name" : "佐藤", "value" : 204, "text" : "キウイフルーツ", "isGT202" : true } { "name" : "武田", "value" : 205, "text" : "スイカ", "isGT202" : true }
となりました。value
が202よりも大きいデータに新しいプロパティが追加されてますね。
複雑な範囲指定をしてUpdate
先ほどのようにfind()
を使って絞り込みができる場合はそれでいいのですが、もっと複雑な検索条件を元にUpdateしたいという場合もあるでしょう。
その場合は直接function()
の中身にif
文を追加します。
text
の文字列の長さが4以上のデータにプロパティtext_length
を追加して、値にtext
の文字列の長さを追加してみます。
db.coll1.find().snapshot().forEach( function(e){ if(e.text.length >=4){ e.text_length = e.text.length; db.coll1.save(e); } } )
その結果は
> db.coll1.find({},{_id:0,isGT202:0}) { "name" : "山田", "value" : 201, "text" : "リンゴ" } { "name" : "田中", "value" : 202, "text" : "バナナ" } { "name" : "加藤", "value" : 203, "text" : "グレープフルーツ", "text_length" : 8 } { "name" : "佐藤", "value" : 204, "text" : "キウイフルーツ", "text_length" : 7 } { "name" : "武田", "value" : 205, "text" : "スイカ" }
となります。文字列の長さが4以上にデータが追加されていることが分かります。
あれ?find()
の部分で条件指定しなくても、function()
の中でif
文使えばすべてできそう…
とは言ったものの、find()
の検索はMongoDBの方で最適化されていると思います。find()
で条件指定ができる部分はできるだけした方がいいかもしれません。
文字列の日付からオブジェクトの日付に更新する
一度文字列で入れてしまった日付をオブジェクトの日付として置き換えます。
> db.coll2.find({},{_id:0}) { "createdAt" : "Mon Oct 09 11:26:20 GMT+09:00 2017" } { "createdAt" : "Mon Oct 09 19:27:09 GMT+09:00 2017" } { "createdAt" : "Mon Oct 02 17:00:42 GMT+09:00 2017" } { "createdAt" : "Tue Aug 29 09:12:49 GMT+09:00 2017" }
Twitterのデータを直接入れると、文字列で格納されてしまうため、日付同士の比較がしにくいですよね。
ですので、オブジェクトの日付に更新しましょう。
db.coll2.find().snapshot().forEach( function(e){ e.createdAt = new Date(e.createdAt); db.coll2.save(e); } )
これを実行すると
> db.coll2.find({},{_id:0}) { "createdAt" : ISODate("2017-10-09T02:26:20Z") } { "createdAt" : ISODate("2017-10-09T10:27:09Z") } { "createdAt" : ISODate("2017-10-02T08:00:42Z") } { "createdAt" : ISODate("2017-08-29T00:12:49Z") }
ISODate
というオブジェクトデータに変わります。ISODataにすると、日本時間を-9時間した標準時間で表記されます。元のデータと違う!という風に焦らないでください。
まとめ
MongoDBの更新はdb.collection.update()
だけでなく、次のようにすることもできます。
db.collection.find().snapshot().forEach(
function(e){
//更新処理
db.collection.save(e);
}
)
function()
の中身ではif
文を使いさらに条件を絞り込んだり、javascript
のコードを使ってデータを操作できます。
データを更新する際は、db.collection.copyTo(newCollection)
等でバックアップを取りましょう!
終わりに
ここまで読んでくださってありがとうございます!
今日もプログラムの話で興味のない方には申し訳ないです。
読者登録、スター、ブックマーク、コメント等ありがとうございます!すごい励みになってます!
またね('ω')ノ