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

# はじめに

[Ruby on Rails](http://rubyonrails.org/)(以下Rails)で[MongoDB](http://www.mongodb.org/)を使う際のORマッパーとして現在は[Mongoid](http://mongoid.org/en/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](http://mongoid.org/en/origin/)が提供するDSLによって、MongoDBのクエリを実行します。Mongoidでは、`Origin::Queryable`を拡張した`Mongoid::Criteria`が[各種のクエリと永続化のメソッド](http://mongoid.org/en/mongoid/docs/querying.html)を提供します。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`

``` ruby
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`

``` ruby
class Venue
  include Mongoid::Document
  include Mongoid::Timestamps
  field :coordinates, type: Array
end
```

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

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

``` erb
  <div class="field">
    <%= f.label :coordinates %><br>
    <%= f.text_field :coordinates %>
  </div>
```

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

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

``` 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`(修正前)

```ruby
    def venue_params
      params.require(:venue).permit(:name, :coordinates)
    end
```

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

```ruby
    def venue_params
      params.require(:venue).permit(:name, :latitude, :longitude)
    end
```

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

```ruby
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](https://github.com/alexreisner/geocoder)などのgemを使うと地名から緯度経度を自動で入力できるはずです。）

![kyoto_venues.png](https://qiita-image-store.s3.amazonaws.com/0/4358/a6e59686-142a-3323-0e48-11eaa84c13f0.png)

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

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

`app/controllers/venues_controller.rb`

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

`app/views/venues/index.html.erb`

```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`

``` ruby
  resources :venues do
    collection do
      post :search
    end
  end
```

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

![venues_search_from_kyoto_st.png](https://qiita-image-store.s3.amazonaws.com/0/4358/c074cd92-ab21-a6b3-fb64-916f7c9ac921.png)

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

# まとめ

本記事ではRailsでMongoidを使う利点について述べ、また実際に位置情報などのMongoidの特徴を生かした簡単なアプリケーションを実際に作ってみました。
本記事をきっかけにActiveRecordだけでなくMongoid、そしてMongoDBの世界に触れる方が増えると幸いです。

# 参考文献

* [Mongoid: Home](http://mongoid.org/en/mongoid/)
* [mongoid/mongoid · GitHub](https://github.com/mongoid/mongoid)
* [icnagy/geolocation_with_mongoid · GitHub](https://github.com/icnagy/geolocation_with_mongoid)