ニートが学ぶプログラミング

ニートの日記。プログラムのことやら、くだらないこと、思ったことをまとめていきます。頑張って毎日更新するぞぉ_(:3」∠)_ 更新が連続して途切れたら察してください。

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のコレクションのデータをもとに、そのコレクションを更新する方法を見つけましたので、紹介したいと思います。

f:id:neet-utsu-taro:20171011211254j:plain

海外の方が解決策を載せてくれてました

stackoverflow.com

色々検索してみると、次のような文章を見つけました。

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" : "スイカ" }

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012141455p:plain

バックアップを取る

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" : "スイカ" }

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012142447p:plain f:id:neet-utsu-taro:20171012142713p:plain

プロパティを加算

プロパティ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されていることが分かります。

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012143810p:plain

指定した範囲だけ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よりも大きいデータに新しいプロパティが追加されてますね。

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012154225p:plain

複雑な範囲指定をして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以上にデータが追加されていることが分かります。

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012155227p:plain

あれ?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" }

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012160540p:plain

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時間した標準時間で表記されます。元のデータと違う!という風に焦らないでください。

クリックしてターミナルの結果を見る

f:id:neet-utsu-taro:20171012161408p:plain

まとめ

MongoDBの更新はdb.collection.update()だけでなく、次のようにすることもできます。

db.collection.find().snapshot().forEach(
  function(e){
    //更新処理 
    db.collection.save(e);
  }
)

function()の中身ではif文を使いさらに条件を絞り込んだり、javascriptのコードを使ってデータを操作できます。

データを更新する際は、db.collection.copyTo(newCollection)等でバックアップを取りましょう!

終わりに

ここまで読んでくださってありがとうございます!

今日もプログラムの話で興味のない方には申し訳ないです。

読者登録、スター、ブックマーク、コメント等ありがとうございます!すごい励みになってます!

またね('ω')ノ