- 2015.02.12
- カテゴリ:
Rails4 + Mongoidでデータ取得するあれこれ
MongoDBのObjectId型を前方検索するのにどう書いたらいいか詰まった流れでいろいろと調べてみました。
■ ObjectId型の_idを前方検索する
▼サンプルデータ
> db.members.find(); { "_id" : ObjectId("54daf7307da4107a3694f5af"), "name" : "hoge", "count" : 1, "status" : 1123, "created_at" : ISODate("2015-02-11T12:20:18.766Z"), "updated_at" : ISODate("2015-02-11T12:20:18.766Z"), "deleted_at" : 0 }
MongoDBの_idはObjectId型になるので、
単純に「/^54d/」みたいな感じではヒットしませんでした。
String型に変換して「ObjectId("」から前方一致するようにすればヒットしますがううーん。
Member.where('$where' => 'function(){return this._id.match(/^ObjectId\(\"%s/)}' % Regexp.escape(@search.id))
と思ったら公式に変換するメソッドが用意されていました。
http://docs.mongodb.org/manual/reference/method/ObjectId.valueOf/
ObjectId.valueOf()を使うとカッコ内のStringを取り出せます。
ObjectId("54daf7307da4107a3694f5af").valueOf() => "54daf7307da4107a3694f5af"
これを使えば↓このように書けます。(※どちらも同じセレクタが生成されます)
Member.where('$where' => 'function(){return this._id.valueOf().match(/^%s/);}' % Regexp.escape(@search.id)) Member.where('$where' => 'this._id.valueOf().match(/^%s/)' % Regexp.escape(@search.id))
さらに、検索条件が$whereのみの場合は$whereも省略可能。
Member.where('this._id.valueOf().match(/^%s/)' % Regexp.escape(@search.id))
使いまわすようであれば、モデルの方でscopeにまとめても良いかと。
コントローラ側 Member.id(@search.id) モデル側 scope :id, ->(id) { 'this._id.valueOf().match(/^%s/)' % Regexp.escape(id) if id.present? }
mongo Shell Methods
http://docs.mongodb.org/manual/reference/method/
IntegerもtoString()で文字列に変換すれば正規表現での検索が可能です。
■ $whereについて
ところで$whereですが。
http://docs.mongodb.org/manual/reference/operator/query/where/
セレクタ内でjavascriptを実行したい場合は$whereを使うようにとのこと。
公式の解説通りですが、ざっくりまとめると以下のような性質があるので
使いどころは考慮する必要があるようです。
- インデックスが使えない
- ネストしたセレクタ内で使えない(トップレベルで使うこと)
- $where以外の条件と併用可能。先に$where以外のセレクタを評価する。(その結果が0の場合$where内は評価されない)
- パフォーマンス的に微妙
- なので$where使わずに済むならそっちを使ってね
- どうしても使うな ら、$where以外の条件も併用して、$whereで評価される対象をある程度絞り込んでおくの推奨
ちなみに完全一致の場合はとてもシンプルで、
Member.where(_id: '54daf7307da4107a3694f5af')
これで↓のようなセレクタが生成されていました。
{"_id"=>{"$all"=>[BSON::ObjectId('54daf7307da4107a3694f5af')]}}
■ 検索条件の指定方法いろいろ
他にもいろいろな指定方法があります。
Mongoid Selection
http://mongoid.org/en/origin/docs/selection.html#standard
http://mongoid.org/en/origin/docs/selection.html#symbol
▼nameに「ho」を含むもの
(String型のフィールドならこの書式で正規表現が有効です)
Member.where(name: /ho/)
▼1と3のもの全部
Member.where(:status.all => [1,3])
▼1と3を含むもの
Member.where(:status.in => [1,3])
▼1と3を含まないもの
Member.where(:status.nin => [1,3])
▼statusがnil false以外
Member.where(:status.exists => true)
▼statusが1より大きい
Member.where(:status.gt => 1)
▼statusが1以上
Member.where(:status.gte => 1)
▼statusが3未満
Member.where(:status.lt => 3)
▼statusが3以下
Member.where(:category.lte => 3)
▼statusが1以外
Member.where(:category.ne => 1)
▼updated_atが2015-02-11 10:15:00.000以降
Member.where(:updated_at.gte => '2015-02-11 10:15')
▼updated_atが2015-02-11 10:15:01.000より前
ミリ秒があるので、「2015-02-11 10:15.725」等にもヒットさせるため1秒後のdatetimeで境界値を含まないようにする
Member.where(:updated_at.lt => Time.at(Time.parse('2015-02-11 10:15').to_i + 1))
■ Mongoidのセレクション以外での指定方法
Model.where()のようなMongoidのセレクション以外にも、
以下のように指定すると、MongoDBのシェルと同様の書式が使えるようです。
db = Mongoid::Sessions.default collection = db[:models] collection.find()
もしくは
Model.collection.find()
※「Mongoid::Sessions.default」はconfig/mongoid.ymlの設定内容?
以下はいずれも同じレコードを取得します。
mongoDB | db.models.find({count: {'$gte' => 1}}) | |
---|---|---|
Rails | Model.where(:count.gte => 1) | Mongoid::Criteria |
Rails | Model.collection.find(count: {'$gte' => 1}) | Moped::Query |
ただし返ってくるオブジェクトが違っていました。
criteria = Model.where(:count.gte => 1) pp criteria.class => Mongoid::Criteria pp criteria.class.ancestors => [Mongoid::Criteria, Mongoid::Sessions::Options, Mongoid::Criteria::Scopable, Mongoid::Criteria::Modifiable, Mongoid::Criteria::Marshalable, Mongoid::Criteria::Inspectable, Mongoid::Criteria::Findable, Origin::Queryable, Origin::Optional, Origin::Selectable, Origin::Aggregable, Origin::Mergeable, Mongoid::Contextual, Enumerable, Object, Delayed::MessageSending, Mongoid::Extensions::Object, BSON::Object, Origin::Extensions::Object, ActiveSupport::Dependencies::Loadable, PP::ObjectMixin, V8::Conversion::Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
moped = Model.collection.find(count: {'$gte' => 1}) pp moped.class => Moped::Query pp moped.class.ancestors => [Moped::Query, Mongoid::QueryCache::Query, Mongoid::QueryCache::Cacheable, Enumerable, Object, Delayed::MessageSending, Mongoid::Extensions::Object, BSON::Object, Origin::Extensions::Object, ActiveSupport::Dependencies::Loadable, PP::ObjectMixin, V8::Conversion::Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
■ aggregateでgroup_byする
MongoDBのシェルが使えるなら↓ここにあるメソッドもそのまま使えるはず…
http://docs.mongodb.org/manual/reference/method/
data = Member.collection.aggregate({'$group' => {'_id' => '$count', 'count' => {'$sum' => 1 }}}) => [{"_id"=>3, "count"=>1}, {"_id"=>2.0, "count"=>2}, {"_id"=>1.0, "count"=>1}]
使えました!戻り値はArrayです。
pp data.class => Array pp data.class.ancestors => [Array, Mongoid::Extensions::Array, BSON::Array, BSON::Encodable, Origin::Extensions::Array, Columnize, V8::Conversion::Array, JSON::Ext::Generator::GeneratorMethods::Array, Enumerable, Object, Delayed::MessageSending, Mongoid::Extensions::Object, BSON::Object, Origin::Extensions::Object, ActiveSupport::Dependencies::Loadable, PP::ObjectMixin, V8::Conversion::Object, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
SQL的なgroup_byとは別ですが、単純に取得結果を特定のキーでグルーピングしたい場合は
Enumerable#group_byが使えます。
(Mongoid::CriteriaもMoped::QueryもEnumerableモジュールを実装してます)
Member.where(:count.gte => 1).group_by { |member| member.status } Member.collection.find(count: {'$gte' => 1}).group_by { |member| member[:status] }
jsファイルを渡して、コンソールからコマンド実行したりもできるらしい…便利ですね。
mongo sample_dev ~/mongo/batch.js
SQLとの比較表も公式にありました。
参考までに。
- kashuuuu!