Edited at

RailsでActiveRecordの代わりにMongoidを使う

More than 3 years have passed since last update.


はじめに

Ruby on Rails(以下Rails)でMongoDBを使う際のORマッパーとして現在はMongoidが標準的に使用されるようになりました。MongoidはActive Recordに似たAPIを備え、かつMongoDBのスキーマレス、ドキュメント指向、ダイナミッククエリ、アトミック操作といった特徴をRubyから容易に扱えるように設計されています。

本記事ではMongoid 4.0.0、Rails 4.1.8をベースに、RailsでMongoidを使う利点を述べ、実際にサンプルアプリケーションを動かして紹介したいと思います。


Mongoidを使う利点

これまでのプロジェクトではActive Recordを介して主にMySQLを使用していましたが、Mongoidを使用するようになって、以下の利点を実感しています。


Active Recordと共通もしくは拡張したクエリと永続化メソッド

Mongoidでは、Originが提供するDSLによって、MongoDBのクエリを実行します。Mongoidでは、Origin::Queryableを拡張したMongoid::Criteria各種のクエリと永続化のメソッドを提供します。CRUD操作はほぼActive Recordと同じようなメソッド呼び出しで行えます。また、その他にMongoDBの各種クエリ演算に対応したメソッドもあり、Arelに似たリッチな文法を使用することが可能です。

たとえば、OR条件を記述できるorメソッドや数値をアトミックにインクリメントして更新するincメソッドなどは実際に使ってみると便利です。


Active Modelでバリデーションが可能

バリデーションに関しては、Active Recordを使用した場合と同様に内部的にはActive Modelを使用しており、ほぼ同様の感覚で使用することができます。


マイグレーションが不要

MongoDBはスキーマレスなので、モデルを変更した際に属性を変更する場合はマイグレーションを実行する必要がありません。この特徴は、特にアプリケーションの初期段階で設計変更が多い場合などに大いに役に立ちます。

インデックスを作成する際も、モデル側に記述を行い、rake db:mongoid:create_indexesコマンドを実行するだけで済みます。


Symbol、ArrayやHashを属性として使用することが可能

MongoidではSymbol、ArrayやHashなどのモデル属性を定義してそのまま永続化することができます。これは特にRubyのシンボルや配列、ハッシュをそのまま保存したいときに便利です。


位置情報など便利機能

MongoDBでは地理空間インデックスを使用できるので、緯度経度などの位置情報を登録しておけば、現在位置から近い順に場所を検索することができます。実際にFoursquareなどのWebサービスで使用されてきた実績もあります。


Mongoidでできないこと

ActiveRecordと異なり、MongoidではテーブルのJOIN、トランザクション処理などはできません。代わりにドキュメント指向のモデル設計を行い、アトミック処理を行うなどの工夫を行う必要があります。これらに関しては、機会があれば記事を書きたいと思います。


実際にMongoidを使ったRailsアプリを作ってみる

前節でRailsでMongoidを使う利点について述べましたが、以下では、実際にごく一部ですが、Mongoidの利点を生かした位置情報アプリケーションのtripperを途中まで作成します。


アプリケーションの作成

まず、railsでアプリケーションを新規作成します。

$ rails new tripper --skip-active-record --skip-bundle

通常と異なるのは、--skip-active-recordを指定することと、bundleコマンドで不要なDB関係のGemをインストールしないことです。

tripperディレクトリに移動し、Gemfileに以下の記述を追加します。

gem 'mongoid', '~> 4.0.0'

bundleコマンドを実行します。

Mongoidの設定ファイルを以下のコマンドで作成します。

$ rails g mongoid:config


Scaffold

rails g scaffoldを実行してオプションを確認すると、自動的にmongoidを使う設定になっています。

Mongoid options:

[--timestamps], [--no-timestamps] # Indicates when to generate timestamps
[--parent=PARENT] # The parent class for the generated model
[--collection=COLLECTION] # The collection for storing model's documents
-t, [--test-framework=NAME] # Test framework to be invoked
# Default: test_unit

旅行のプラン名を記述するPlanモデルを作成します。create_at, updated_atを追加するために--timestampsを追加します。

また、標準で文字列になるため、titleに型は指定していません。

$ rails g scaffold plan title --timestamps

ここで、モデルファイルは以下のようになっています。

app/models/plan.rb

class Plan

include Mongoid::Document
include Mongoid::Timestamps
field :title, type: String
end

ここで、Active Recordと異なりマイグレーションを実行する必要がありません。そのまま以下のコマンドで実際にアプリケーションを起動することができます。

rails s

コントローラapp/controllers/plans_controller.rbのメソッドのCRUD操作もActive Recordと同じになっていることを確認してください。


Mongoidの独自機能(Array属性, 位置情報)を使う

ここで、旅行で訪れる場所を記録するために、Venueモデルを作成します。

rails g scaffold venue name coordinates:Array --timestamps

Venueモデルは以下のようになります。

app/models/venue.rb

class Venue

include Mongoid::Document
include Mongoid::Timestamps
field :coordinates, type: Array
end

ここで、Scaffoldが生成したviewとcontrollerではそのままArrayのfieldを扱うことができないので、以下の部分のコードを

app/views/venues/_form.html.erb (修正前)

  <div class="field">

<%= f.label :coordinates %><br>
<%= f.text_field :coordinates %>
</div>

以下のように修正します。

app/views/venues/_form.html.erb (修正後)

  <div class="field">

<%= f.label :latitude %><br>
<%= f.text_field :latitude %>
</div>
<div class="field">
<%= f.label :longitude %><br>
<%= f.text_field :longitude %>
</div>

また、controllerの以下のコードを

app/controllers/venues_controller.rb(修正前)

    def venue_params

params.require(:venue).permit(:name, :coordinates)
end

以下のように変更します。

    def venue_params

params.require(:venue).permit(:name, :latitude, :longitude)
end

Venueモデルにアクセッサとコールバック、インデックスの定義を行い、位置情報インデックスを使用できるようにします。

class Venue

...
attr_accessor :latitude, :longitude
index({ coordinates: '2dsphere' })

after_validation :set_coordinates

private
def set_coordinates
self.coordinates = [longitude.to_f, latitude.to_f]
end
...

ここでインデックスの作成を行います。

$ rake db:mongoid:create_indexes

I, [2014-12-15T19:33:26.240958 #2351] INFO -- : MONGOID: Created indexes on Venue:
I, [2014-12-15T19:33:26.241082 #2351] INFO -- : MONGOID: Index: {:coordinates=>"2dsphere"}, Options: {}

これで位置情報を登録する準備ができました。

rails sでサーバを起動し、http://localhost:3000/venues にアクセスして、適当な位置情報を登録します。今回は以下のような京都の場所を登録しました。(今回は手動で入力しましたがgeocoderなどのgemを使うと地名から緯度経度を自動で入力できるはずです。)


位置情報からの検索・並べ替えを行う

上記の登録した場所を、ある場所から近い順に並べ替えを行い一番近い場所を検索したいと思います。上記でインデックスを作成したので、Mongoidの#geo_nearメソッドを使えば簡単に実装ができます。例えば、http://localhost:3000/venues/search から検索を行いたい場合、以下のようにコードを追加、修正します。

app/controllers/venues_controller.rb

  def search

@venues = Venue.geo_near([params[:longitude].to_f, params[:latitude].to_f]).spherical
render 'index'
end

app/views/venues/index.html.erb

<%= form_tag search_venues_path do -%>

<%= label_tag 'longitude' %>
<%= text_field_tag 'longitude' %>
<%= label_tag 'latitude' %>
<%= text_field_tag 'latitude' %>
<%= submit_tag 'sort' %>
<% end -%>

config/routes.rb

  resources :venues do

collection do
post :search
end
end

http://localhost:3000/venues からアクセスして、京都駅付近の緯度経度を入力すると、4つの場所の中で産寧坂が最も近いことがわかります。

アプリケーションの実装は途中ですが、PlanとVenueをembeds_manyで関連付けを行えば、行き先リストを作るのも可能だと思います。ドキュメント指向のモデル作成などここで取り上げなかったトピックについても試してみてください。


まとめ

本記事ではRailsでMongoidを使う利点について述べ、また実際に位置情報などのMongoidの特徴を生かした簡単なアプリケーションを実際に作ってみました。

本記事をきっかけにActiveRecordだけでなくMongoid、そしてMongoDBの世界に触れる方が増えると幸いです。


参考文献