[Rails4/5]はじめてのMountable Engine

# 前提
アプリケーションの規模が大きくなりつつある段階で、アプリの中核ではない機能（今回はブログ記事管理機能）をプラグインとして切り出したいなーと思い、RailsのMountable Engine型プラグインを作ってみました。
「はじめての」なので、基礎的な内容になります。

# これからやること
- Mountableエンジンを作成する
- Scaffoldで記事管理機能を作成する
- エンジンに親アプリとは別のassetを適用する
- テストにRSpecを使えるように設定する
- FactoryGirlをRSpec内で使う

# サンプルアプリ
https://github.com/kaorina/engine-rails

# Mountableエンジンとは？

Railsアプリケーション内に存在するが、完全に本体アプリ（親）から独立した小さなアプリケーションのこと。
Model、View、Controller、ルーティング、Gemfile、assetなど全てにおいて**親アプリとは別の世界で管理**している。
そして、**親アプリのそれらを利用すること**もできる。

例えば、もし親が膨大なアプリに成長してしまった際に、親に実装進めて更に太らせていくのではなく、独立できる機能はMountableエンジンとして別階層に切り出して実装するという策をとると、親がカオスに陥ることがなくHappyなことが多いらしい。何の機能をエンジン化するかはじっくり検討し設計が必要そうだけど。
（コンテンツの管理機能や、お問い合わせ機能なども管理方法によっては対象にできそう。）

# 1. Mountableエンジンを作成する
今回ブログ記事の管理ツールを親アプリケーションから切り離しエンジン化するという想定で、**blog_engine**という名前のMountableエンジンを作成する。
エンジン名は命名ルールとして**小文字かつアンダースコアを使うこと。**

blog_engineというプラグインをGemとして作成し、親アプリ側でそのGemをインストール、ルーティングの設定でガシャーンとmountすることで、使えるようになるというイメージです！

## 作成コマンドを実行
親アプリのディレクトリに移動し、plugin作成コマンドを実行

```
$ ./bin/rails plugin new blog_engine --mountable
```

テストにRSpecを使いたい場合はこの段階で、オプション付きで作成しておくのがベター

```
$ ./bin/rails plugin new blog_engine --mountable -T --dummy-path=spec/dummy_app
```

実行すると、`app`ディレクトリと同列に`blog_engine`ディレクトリが作成される
中身を見てみると、appを始めとして見慣れたディレクトリが並んでいるのがわかる。
 
Gemfileをみると

```
gemspec
```

と記述があり、同列にある `blog_engine.gemspec`を参照しているようだ。

## gemspecを編集する
[TO DO] となっている箇所、email, homepageをアプリに沿った内容に変更する
下記のilovebeerはサンプルの親アプリ名なのでお好きなように設定。

blog_engine/blog_engine.gemspec

```
$:.push File.expand_path("../lib", __FILE__)

# Maintain your gem's version:
require "blog_engine/version"

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
  s.name        = "blog_engine"
  s.version     = BlogEngine::VERSION
  s.authors     = ["ilovebeer"]                  
  s.email       = ["info@ilovebeer.com"]
  s.homepage    = "http://ilovebeer.com"
  s.summary     = "ilovebeer: Summary of BlogEngine."
  s.description = "ilovebeer: Description of BlogEngine."
  s.license     = "MIT"
  
  s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
  s.test_files = Dir["test/**/*"]

  s.add_dependency "rails", "~> 4.2.0"

  s.add_development_dependency "sqlite3"
end
```

blog_engine以下へ移動し、bundle installを実行

```
$ cd blog_engine
$ bundle install
```
## 親アプリにGemをインストールする
エンジンを使えるように親アプリ側に設定します。これをやらないと作ったエンジンが使えません！
親アプリ側のGemfileにgem blog_engine を読み込ませる

Gemfile

```
gem 'blog_engine', path: 'blog_engine'
（他記述は省略）
```

今回はアプリ内に作成したのでpathは上記のようになっているが、作る場所によって異なる。
また、gitHub上に公開して、gitのURLを指定する場合が一般的のようです。

```
$ cd ../
$ bundle install
```

## エンジンをmountする
親アプリ側のルートへ追加。

config/routes.rb

``` 
require 'sidekiq/web'
Rails.application.routes.draw do
  mount BlogEngine::Engine, at: '/blog'
（他記述は省略）
end
```
`at` はどのURLでアクセスするかを設定。今回は http://localhost:3000/blog でエンジンにアクセスできるように設定しました。
ただし、この時点ではサーバーを起動しアクセスしてもRouting Errorになってしまいます。
理由としてエンジン側のroutes.rbの記述が必要ですが、それについては「2. Scaffoldで記事管理機能を作成する」で行います。

## エンジンが正しくmountされたことを確認
サーバーを起動し、エンジンのマウント先URLにアクセスするとRouting Errorが出ることを確認。
その際、ブラウザに表示されているRoutesに設定したエンジンのパスが表示されていれば正しくマウントできています。

![RoutingError画面.png](https://qiita-image-store.s3.amazonaws.com/0/111637/c778c732-e1ac-a915-cb83-8a23b330314d.png)

# 2. Scaffoldで記事管理機能を作成する
エンジンで利用するModel, View, Controllerを作成していきます。

## Scaffoldを実行
Articleという記事管理用のモデルを作成し、Articleテーブルには titiel:string型, body:text型のカラムをもたせます。

```
$ cd blog_engine
$ bundle exec rails g scaffold articles title body:text
```

## Migrationファイルを親アプリ側へコピー
scaffoldコマンドで作られた`blog_engine/db/migrate/`以下のマイグレーションファイルを親アプリへコピーします。

```
$ cd ../
$ rake blog_engine:install:migrations
```

これでアプリ側に下記ファイルができる
db/migrate/xxxx_create_blog_engine_articles.**blog_engine**.rb
お尻にエンジン名のscopeがついているのがポイント。

## Migrationを実行し、コンソールで確認

```
$ bin/rake db:migrate
```
これでエンジン以下にArticleテーブルが作成されたので、コンソールで確認してみましょう。

```
$ rails c

# これはアプリ側のArticleテーブルを呼ぶのでエンジンではない
pry(main)> Article

# 頭にエンジン名をつけて呼び出す
pry(main)> BlogEngine::Article

pry(main)> BlogEngine::Article.first
  BlogEngine::Article Load (0.4ms)  SELECT  `blog_engine_articles`.* FROM `blog_engine_articles`  ORDER BY `blog_engine_articles`.`id` ASC LIMIT 1
=> nil
```
参照できました！

## サーバーを起動し、ブラウザで確認
ルーティングを確認し、`/blog/articles` でindexのページにアクセスしてみる。

New Articleリンクから記事を作成できることを確認〜。
![scaffold_article.png](https://qiita-image-store.s3.amazonaws.com/0/111637/bd9a304e-4711-1265-4038-af66827a7c69.png)

## エンジン以下のapplication.jsを修正
`jquery`, `jquery_ujs` を呼び出す記述を追加

blog_engine/app/assets/javascripts/blog_engine/application.js
（※先頭が +の行は追加、 -の行は削除を意味しています）

```
+ //= require jquery
+ //= require jquery_ujs
//= require_tree .
```

**これを書かないとScaffoldが正しく動かない（Destroyでdialogが立ち上がらず削除できない）**
また、この記述だとエンジン側でなく**親アプリ側**のassetパスからjQuery達を参照している。
アプリ側にもjQuery関連のGemがインストールされているので問題なく動いているが、ない場合はエラーになるので、エンジン側のassetパスを参照するようにする必要がある。
その方法は次のステップで行います。
問題なくブラウザ上でAarticleの各アクションが動くことを確認できればOKです。


# 3. エンジンに親アプリとは別のassetを適用する
今回はサンプルお題として、エンジン側にBootStrapを適用します。

要件として

- 親アプリ側のasset（CSS, JavaScript）を呼び出すのではなく、**エンジン側で独自のassetを適用したい場合**を想定して進める。

- 今回BootStrapの適用方法については、**Gemを利用する方式でなく、公式サイトからダウンロードしたソースファイルを使って**実装する。

つまり、**エンジン側の**asset配下に置いたCSS, JavaScriptファイルを読み込む方法を説明します。

## ダウンロードしたソースをエンジンのassetへ設置
[Bootstrap4](https://v4-alpha.getbootstrap.com/getting-started/download/)のページからsource filesをダウンロードし、ZIPを解凍。

scssフォルダの中身をごっそり下記ディレクトリへ置く
`blog_engine/app/assets/stylesheets/blog_engine`
（cssファイルでも良いが、今回はscssを利用）

`dist/js`フォルダの`bootstrap.min.js` を下記ディレクトリに置く
`blog_engine/app/assets/javascripts/blog_engine`

**stylesheets, javascriptsディレクトリ以下にblog_engine用のディレクトリが切られているので、その配下に置くのがポイント！**

また、Bootstrap4のjsファイルを使う場合、単品だとChromeのコンソールにエラーが出てしまう。[StackOverFlowより](https://stackoverflow.com/questions/34567939/how-to-fix-the-error-error-bootstrap-tooltips-require-tether-http-github-h)

解決には[Tether](http://tether.io/)というライブラリが必要とのことなので、「Download ZIP」からダウンロードし、`tether.min.js`をjsファイルと同じ場所に設置する。

## 設置したassetファイルを読み込むように追記 [ハマりどころ]
**scss**
blog_engine/app/assets/stylesheets/blog_engine/application.scss
（↑拡張子はcssのままでも、scssに変えてもどちらでも良いぽい）


```
+ *= require blog_engine/bootstrap
- *= require_tree .
*= require_self
```

**JavaScript**
blog_engine/app/assets/javascripts/blog_engine/application.js

```
//= require jquery
//= require jquery_ujs
+ //= require blog_engine/tether.min
+ //= require blog_engine/bootstrap.min
- //= require_tree .
```

**両方とも、ポイントは blog_engine/を明記すること！**
**ファイル名だけを書いた場合、親アプリ側のassetパスを指していることになります！**
知ってたらふーんという感じですが、知らなかったので地味にハマりました。。

参考までに、どうパスを書けば読み込まれるか？の検証をする場合、いきなりBootstrapのファイルを適用する前にもっとライトなやり方として
scaffold実行時にできた `blog_engine/app/assets/javascripts/blog_engine/article.js`を利用して
1. 簡単なJavaScriptのコードを書いてみてる
2. `application.js` にどう書けば読み込まれるか試行錯誤しながらブラウザで確認してみる〜というステップを踏みました。

結果的にこの書き方でうまくいくかと。
blog_engine/app/assets/javascripts/blog_engine/articles.js

```
console.log("Hi")
```

blog_engine/app/assets/javascripts/blog_engine/application.js

```
+ //= require blog_engine/articles
```


表示された！！
![index_bootstrap_2.png](https://qiita-image-store.s3.amazonaws.com/0/111637/a3a133a0-13e0-1930-622c-1400af296985.png)



デフォルトで書いてある `//= require_tree .` については、この場合**親アプリ側の**assetsパス配下にあるcss, jsファイル全てを読み込むという意味になってしまいます。
この記述を残していると、（明確な理由はわかりませんが、おそらくアプリ側とエンジン側のcss, jsファイルの中身の定義がバッティングしているとかで）Sass::SyntaxError になってしまったので削除しました。

## BootStrapが適用されているかどうかViewを修正し確認
index用のviewを修正
blog_engine/app/views/blog_engine/articles/index.html.erb

scssの適用を確認するために、Showリンクにクラスを追加してみる

```
<%= link_to 'Show', article, class: "btn btn-primary" %>
```

JavaScriptの適用を確認するために、ドロップダウンを設置してみる

```
<div class="dropdown">
    <button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
      Dropdown sample
      <span class="caret"></span>
    </button>
    <ul class="dropdown-menu" aria-labelledby="dLabel">
      <li>Menu A</li>
      <li>Menu B</li>
      <li>Menu C</li>
    </ul>
  </div>
```

Showリンク、ドロップダウンが下記のように表示されれば適用できています！

![index_bootstrap.png](https://qiita-image-store.s3.amazonaws.com/0/111637/b98a54cd-75ba-d46c-bb2b-dd36bb0cc71e.png)


# 4. RSpecを使えるように設定する

MountableエンジンのテストはデフォルトがRSpecではないので、設定が必要。

## テストをRSpecに指定しエンジンを作成
「1. Mountableエンジンを作成する」 の作成時に、テストをRSpecにするオプション付きのコマンドを実行する。

## すでにオプション無しでエンジンを作成してしまった場合
オプションをつけずにエンジンを作成した場合は、blog_engineとは別のエンジンを新規作成し、
新規作成したエンジンのディレクトリ以下から必要なものをblog_engine側にコピーする策をとる。

**親アプリのディレクトリへ移動し、下記実行**
例としてblog_engine2というエンジンを作成

```
$ ./bin/rails plugin new blog_engine2 --mountable -T --dummy-path=spec/dummy_app
```

**オプションについて**

- -T  (--skip-test-unit)
- --dummy-path 
**テストを実行するダミーのアプリ**を作成する。
エンジンの場合、テストを実行するためにspec以下にダミーのアプリを作成し、それをmountすることで、テスト可能なエンジンの環境を作る。

**ディレクトリをコピペして、ファイルを修正**

- blog_engine2/spec/dummy_appディレクトリをコピーし、blog_engine/spec/dummy_appを作成。
- dummy_app以下でblog_engine2と記載がある箇所をblog_engineへ修正
- 不要になったので、blog_engine/test ディレクトリを削除

## gemspecファイルを修正
必要なGemを追加し、bundle installを実行

blog_engine/blog_engine.gemspec

```
$:.push File.expand_path("../lib", __FILE__)
（中略）
-  s.test_files = Dir["test/**/*"]
+ s.test_files = Dir["spec/**/*"]
s.add_dependency "rails", "~> 4.2.0"
s.add_development_dependency "sqlite3"
+ s.add_development_dependency "rspec-rails"
+ s.add_development_dependency "factory_girl_rails"
```

```
$ cd blog_engine/
$ bundle install
```

## engine.rb に設定を追加

blog_engine/lib/blog_engine/engine.rb

```
module BlogEngine
  class Engine < ::Rails::Engine
    isolate_namespace BlogEngine

    + config.generators do |g|
    +   g.test_framework :rspec, fixture: false
    +   g.fixture_replacement :factory_girl, dir: "spec/factories"
    + end
  end
end
```

## blog_engineへRSpecをインストール
下記コマンドでRSpec実行に必要なファイル等を作成

```
$ rails g rspec:install
```

## rails_helper.rb を修正
blog_engine/spec/rails_helper.rb

```
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV['RAILS_ENV'] ||= 'test'
require 'spec_helper'
- require File.expand_path("../../config/environment", __FILE__)

# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
  - config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end
```

## spec_helper.rb を修正
blog_engine/spec/spec_helper.rb

```
ENV['RAILS_ENV'] ||= 'test'

+ require File.expand_path("../dummy_app/config/environment.rb", __FILE__)
require 'rspec/rails'
+ require 'factory_girl_rails'

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.shared_context_metadata_behavior = :apply_to_host_groups
end
```

## 簡単なspecを作成してみる
blog_engine/spec/controllers/articles_controller_spec.rb

```
describe BlogEngine::ArticlesController, type: :controller do
  routes { BlogEngine::Engine.routes }
  describe "GET /articles" do
    it "works! (now write some real specs)" do
      get :index
      expect(response).to have_http_status(200)
    end
  end
end
```

実行できるとこんな画面になります。
![spec.png](https://qiita-image-store.s3.amazonaws.com/0/111637/2d742d4e-f8b0-3e5f-c935-7316120a2170.png)

実行できたが結果の詳細が表示されない場合は、RSpecの結果をドキュメント形式で出力する設定を加えてください。

blog_engine/.rspec

```
+ --format documentation
```
<br>
**specファイルについてのポイントは、**

- describeの部分は**BlogEngine::**ArticlesController　とエンジン名のscopeをControllerの先頭につける
- 通常のController specの場合 get :index のみで良いが、エンジンの場合は **routes { BlogEngine::Engine.routes }** で先にルーティングを読み込ませる記述を書いておかないとエラーになってしまう。


### また、設定ファイル rails_helper.rb, spec_helper.rbついて

２つあるけどどちらに何を書けばいい？
私はよく分かってなかったのでハマりました〜。

迷いなく `rails_helper.rb`にRSpecの設定を全て書いて、テストを実行したけど動かない〜という状況に陥っていました。
**RSpec実行時にspec_helper.rbが先に読み込まれるので、spec_helperにはRSpecの設定を全て書いておく必要がある**とのこと！

区別的に下記の認識がわかりやすいです。（参考記事より）

> spec/rails_helper.rb には**Rails特有の設定**
> spec/spec_helper.rbには**RSpecの全体的な設定** を記載する

試しに、`rails_helper.rb`の記述を`spec_helper.rb`にコピーし、`rails_helper.rb`の中身を空にしてテストを実行してみたりしました。
この場合テストはクリアしましたが、逆の場合は（ `spec_helper.rb`を空にしてみると）エラーになりました。ふむふむ。

# 5. FactoryGirlをRSpec内で使う
以下のステップに分けて設定を進めていきます。

- 1. まず、コンソールでFactoryコマンドを実行できるようにする
- 2. 次にRSpec内でFactoryGirlを使ってみる

## 1. コンソールでFactoryコマンドを実行できるようにする

## 親アプリ側にFactoryGirlのGemをインストールしておく
Gemfile

```
+ group :development, :test do
+  gem 'factory_girl_rails'
+ end
```

## Factoryを作成
ArticleテーブルについてFactoryを定義するファイルを作成し、Factoryを作成。

blog_engine/spec/factories/blog_engine_articles.rb

```
FactoryGirl.define do
  factory :blog_engine_article, class: BlogEngine::Article do
    sequence(:title) { |n| "記事のタイトル#{n}" }
    body "Boddddddy"
  end
end
```

**ポイントは**

- factory名　親アプリ、エンジンを通して**一意である必要があるので**、既に使っているfactory名を避けて名前をつける
- class名をつける　コンソールでモデルの参照を行ったときと同様に**エンジン名のscopeをつけてモデル名を書く**

この状態でコンソールで

```
FactoryGirl.build(:blog_engine_article)
```
が通ると思ったが、通らず。。
更に調べてみると、
**gmespec, engine.rbへのfactoryのパス追加が必要だった〜。**

## gemspecを修正
spec/factoriesへのパスを追加

blog_engine/blog_engine.gemspec

```
-  s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
+  s.files = Dir["{app,config,db,lib}/**/*", "spec/factories/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
-  s.test_files = Dir["test/**/*"]
+  s.test_files = Dir["spec/**/*"]
```

## engine.rbを修正
blog_engine/lib/blog_engine/engine.rb

```
+    initializer "model_core.factories", :after => "factory_girl.set_factory_paths" do
+      FactoryGirl.definition_file_paths << File.expand_path('../../../spec/factories', __FILE__) if defined?(FactoryGirl)
+    end
```

そして、再度コンソールで実行すると...無事インスタンスを作成できた！
![console_FG.png](https://qiita-image-store.s3.amazonaws.com/0/111637/a5b66ffe-8e67-d809-fef8-92b704781fa1.png)

## 2. RSpec内でFactoryGirlを使ってみる [ハマりどころ]

articles_controllerのspecにFactoryGirlの記述を追加

blog_engine/spec/controllers/articles_controller_spec.rb

```
describe BlogEngine::ArticlesController, type: :controller do
  routes { BlogEngine::Engine.routes }
  describe "GET /articles" do
    it "works! (now write some real specs)" do
      get :index
      + FactoryGirl.create(:blog_engine_article)
      expect(response).to have_http_status(200)
    end
  end
end
```

この状態でspecを実行するとまだエラーになってしまう。
この時点でなぜテストが通らないかでハマった〜>_<

Rspecでハマったときは...
**binding.pry** をspecファイルや、実際のコード（この場合articles_controller.rb）に書いてみて原因を探るのが良いと教わったので、試行錯誤してみる。
ただし、この状態ではpryは使えないのでGemfileに下記を追加し、エンジン以下でbundle installが必要

blog_engine/Gemfile

```
+ group :development, :test do
  + gem 'pry-rails'
  + gem 'rb-readline' #環境によってはこちらも必要
+ end
```
これでbinding.pryが使えるようになった。

そしてRSpec内でFactoryGirlを使うには結論として下記が必要だった！

- spec_helperにfactory_girl_railsを記載していることを確認
- Rakefileを確認
- テスト用DBの作成、migration実行

## spec_helper.rbを確認
factory_girl_railsが参照されていることを確認。記載していなければ追加。
ついでにFactoryGirlの呼び出しを短く書ける記述（FactoryGirl.xxxの頭の”FactoryGirl”を省略する）も追加しておく。

```
+ require 'factory_girl_rails'
RSpec.configure do |config|
  + config.include FactoryGirl::Syntax::Methods
end 
```

## Rakefileを修正
「1. Mountableエンジンを作成する」 でのエンジン作成の際、RSpecを使うオプション付きで作成した場合は修正不要。
既に下記の内容になっていると思います。

blog_engine/Rakefile

```
begin
  require 'bundler/setup'
rescue LoadError
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end

require 'rdoc/task'

RDoc::Task.new(:rdoc) do |rdoc|
  rdoc.rdoc_dir = 'rdoc'
  rdoc.title    = 'BlogEngine'
  rdoc.options << '--line-numbers'
  rdoc.rdoc_files.include('README.md')
  rdoc.rdoc_files.include('lib/**/*.rb')
end

APP_RAKEFILE = File.expand_path("../spec/dummy_app/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'
load 'rails/tasks/statistics.rake'
require 'bundler/gem_tasks'
```

## テスト用DBの作成、migration実行

```
$ cd blog_engine
$ RAILS_ENV=test rake db:create
$ RAILS_ENV=test rake db:migrate
```

テスト実行するとパスできました！やった！

```
$ bundle exec rspec spec/controllers/
```

ちなみにspec内のFactoryGirl.createの記述は下記でOK。

```
create(:blog_engine_article)
```

# まとめ： 実際に作ってみて
### 思ったより簡単だった所
Mountableエンジンを作成し、動かすとこまではスムーズにできた。デフォルトの使い方はわかりやすいなと思いました。
エンジン以下のディレクトリ構成などは見慣れたものが多く、構成は理解しやすかった。

### イマイチ概念がわからずモヤモヤしたところ

- テスト環境としてspec以下にダミーのアプリを持つという概念が、エンジン特有で理解し難かった。
- 親アプリに極力依存せず、**親とは別のものを使いたい**となった時のカスタマイズがわかりずらかった。（assetの参照など）

- （想像だが）更に実装を進めていくと、どの程度親側のもの（Model等）を共通で使い、どれをエンジン側で自前で持つかの切り分けについて、ルール化しながら実装を進める必要があるのかなーと思いました。
 

## 参考情報

- わかりやすくMountableエンジンの始め方が載っています
<br>
http://blog.onk.ninja/2014/12/02/mountable_engine

> Rails アプリ内に Rails アプリを入れ子で実現する仕組みです。 
よく使われるのは RailsAdmin, ActiveAdmin といった管理画面の分離ですね。
なぜこういう作りにするのかと言うと
関心ごとを分離できる、標準化される、アプリを小さく保てるからです。

- 鉄板Railsガイド （ちょっと難しかったので部分的に参考にしました）
<br>
https://railsguides.jp/engines.html

> アプリケーションは いかなる場合も エンジンよりも優先されます。
ある環境において、最終的な決定権を持つのはアプリケーション自身です。
エンジンはアプリケーションの動作を大幅に変更するものではなく、アプリケーションを単に拡張するものです。


- 書籍 パーフェクトRuby on Rails
<br>
Part4でさらっと書いてあります。作ってみた後に読むと更になるほどと。