Skip to content

LearningRailsWithTriglav03

Gosuke Miyashita edited this page Feb 27, 2013 · 1 revision

Triglav で学ぶ Ruby on Rails & GitHub 第3回

今回は Rails のディレクトリ構造と MVC について簡単に学んで、M, V, C のファイルを作成して機能追加してみます。


MVC について

MVC(Model View Controller) とは、アプリケーションアーキテクチャパターンの一種です。

具体的な内容については、ホワイトボードで図を書きながら説明します。


Rails のディレクトリ構造

Triglav の中で、MVC に関連のあるディレクトリとファイルは以下のような構成になっています。(一部のみ抜粋しています。)

app
|-- controllers
|   |-- activities_controller.rb
|   |-- api_controller.rb
|   `-- application_controller.rb
|-- models
|   |-- activity.rb
|   `-- user.rb
`-- views
    |-- activities
    |   `-- index.html.erb
    |-- api
    |   `-- index.html.erb
    `-- layouts
        `-- application.html.erb

controllers の下にはコントローラが、models の下にはモデルが、views の下にはコントローラ/アクションに対応したビューが配置されます。


コントローラとアクション

コントローラは更にアクションという単位に細分化されます。

例えば、app/controllers/hosts_controller.rb の中身を見てみると、以下のようになっています。

class HostsController < ApplicationController
  respond_to :html, :json

  def index
    @hosts_without_deleted = Host.without_deleted
    @deleted_hosts = Host.deleted
    respond_with @hosts_without_deleted
  end

  def show
    @host  = Host.find_by_name(params[:id])
    @munin = Munin.new

    respond_with @host
  end

  ... 以下略 ...

class HostsController がコントローラの定義、def indexdef show がアクションの定義です。


Rails のルーティング

Rails では、HTTP メソッドや URL によって、呼び出されるコントローラ/アクションが決まります。HTTP メソッドや URL と、コントローラ/アクションを紐づけることをルーティングと呼びます。ルーティングは config/routes.rb で定義されています。

Triglav::Application.routes.draw do
  root 'root#index'
  get '/caveat', to: 'root#caveat'

  get    '/signin' => redirect('/auth/github')
  delete '/signout', to: 'sessions#destroy'
  get    '/auth/:provider/callback', to: 'sessions#create'
  if Rails.env.development?
    get    '/dev_signin' => redirect('/auth/developer')
    post   '/auth/developer/callback', to: 'sessions#create'
  end

  resources :users, constraints: { id: /[^\/\.]+/ }, only: %w(show update)

  ... 以下略 ...
end

どのメソッドや URL に、どういったコントローラ/アクションが紐付いているかは、rake routes コマンドで確認できます。

$ rake routes
                   root GET    /                                                    root#index
                 caveat GET    /caveat(.:format)                                    root#caveat
                 signin GET    /signin(.:format)                                    redirect(301, /auth/github)

... 中略 ...

            revert_host PUT    /hosts/:id/revert(.:format)                          hosts#revert {:id=>/[^\/]+/}
          host_comments POST   /hosts/:host_id/comments(.:format)                   comments#create {:id=>/[^\/]+/, :host_id=>/[^\/]+/}
                  hosts GET    /hosts(.:format)                                     hosts#index {:id=>/[^\/]+/}
                        POST   /hosts(.:format)                                     hosts#create {:id=>/[^\/]+/}
               new_host GET    /hosts/new(.:format)                                 hosts#new {:id=>/[^\/]+/}
              edit_host GET    /hosts/:id/edit(.:format)                            hosts#edit {:id=>/[^\/]+/}
                   host GET    /hosts/:id(.:format)                                 hosts#show {:id=>/[^\/]+/}
                        PATCH  /hosts/:id(.:format)                                 hosts#update {:id=>/[^\/]+/}
                        PUT    /hosts/:id(.:format)                                 hosts#update {:id=>/[^\/]+/}
                        DELETE /hosts/:id(.:format)                                 hosts#destroy {:id=>/[^\/]+/}

... 以下略 ...

ルーティングはちょっとややこしいので、今回は深くは触れません。


hosts_contorller を例に MVC の動きを見てみる

もう一度 app/controllers/hosts_controller.rb の中身を見てみます。

class HostsController < ApplicationController
  respond_to :html, :json

  def index
    @hosts_without_deleted = Host.without_deleted
    @deleted_hosts = Host.deleted
    respond_with @hosts_without_deleted
  end

  def show
    @host  = Host.find_by_name(params[:id])
    @munin = Munin.new

    respond_with @host
  end

  ... 以下略 ...

/hosts にアクセスした場合、このコントローラの中の index アクションが呼び出されます。また、/hosts/users001.sqale.jp といった形でアクセスすると、show アクションが呼び出されます。(rake routes で確認してみましょう。)

最初の方にある respond_to :html, :json は、入力/出力フォーマットとして、HTML と JSON に対応することを表しています。

たとえば、/hosts または /hosts.html にアクセスすると HTML がレスポンスボディとして返り、/hosts.json にアクセスすると、JSON がレスポンスボディとして返ります。実際にアクセスして試してみてください。

出力フォーマットは .format の形で URL 中で指定しますが、指定がない場合には、.html が指定されていると解釈されます。

コントローラ/アクションが呼び出されると、その中で定義されている処理が呼び出されます。

/hosts にアクセスした場合は def index ... end 内の処理が呼び出されます。

この中で使われているモデル Host の実体が、app/models/host.rb で、以下のような内容になってます。

class Host < ActiveRecord::Base
  include ActiveModel::ForbiddenAttributesProtection
  include LogicallyDeletableRole
  include HasHostRelationsRole
  include ::HasDeclarativePathRole

  validates :name,        presence: true, uniqueness: true, length: { maximum:  100 }, format: { with: /\A[^\/]+\Z/ }
  validates :ip_address,  format: { with: /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/ }, allow_blank: true
  validates :description, length: { maximum: 255 }

  has_many :host_relations, dependent: :delete_all
  has_many :services, through: :host_relations
  has_many :roles,    through: :host_relations
  has_many :activities, as: :model
  has_many :comments,   as: :model

  accepts_nested_attributes_for :host_relations,
    allow_destroy: true,
    reject_if: lambda{ |attrs|
      attrs[:service_id].blank? || attrs[:role_id].blank?
    }
end

このモデルクラスは ActiveRecord という、データベースを扱うクラスを継承していますが、内容については、今回は深くは触れません。

もう1度 index アクションの中を見てみます。

  def index
    @hosts_without_deleted = Host.without_deleted
    @deleted_hosts = Host.deleted
    respond_with @hosts_without_deleted
  end

@hosts_without_deleted = Host.without_deleted で、変数 @hosts_without_deleted に Host モデルの without_deleted メソッドの実行結果を代入しています。また、変数 @deleted_hosts に Host モデルの deleted メソッドの実行結果を代入しています。

最後に、respond_with @hosts_without_deleted で、ビューの処理を行っています。この処理では、/hosts にアクセスした場合には、フォーマットが HTML であるので、app/views/hosts/index.html.erb というテンプレートを描画します。テンプレートの内容は以下のようになっています。

<%- model_class = Host -%>
<div class="page-header">
  <h1><%= model_class.model_name.human(count: 2) %></h1>
</div>

<%= render partial: 'shared/hosts', locals: { hosts: @hosts_without_deleted, is_deletable: true } %>
<%= link_to t('helpers.links.new'), new_host_path, class: 'btn btn-primary lsf-icon', title: 'plus' %>

<% unless @deleted_hosts.blank? -%>
  <hr />
  <h2><%= t('.destroyed_hosts') %></h2>
  <%= render partial: 'shared/hosts', locals: { hosts: @deleted_hosts, is_deletable: false } %>
<% end -%>

この中で、先ほど出てきた変数 @hosts_without_deleted@deleted_hosts が使われています。

では、/hosts.json にアクセスした場合は、どのような動きになるでしょうか?この場合、最後の respond_with @hosts_without_deleted の部分の動きが変わります。

まず、JSON フォーマットに対応したテンプレート app/views/hosts/index.json.erb を探して描画しようとします。ところが、このファイルは存在しません。そこで Rails は respond_with に引数として与えられた変数 @hosts_without_deleted の内容を、JSON フォーマットに変換して出力します。結果として、以下のようなレスポンスが返ります。

[{"host":{"id":1,"ip_address":"","name":"users001.sqale.jp","description":"","created_at":"2012-11-27T09:42:26Z","updated_at":"2012-11-27T09:42:26Z","deleted_at":null,"active":true}}]

M, V, C のファイルを追加して、簡単な機能を追加してみる

Triglav にミドルウェアの名前とバージョンを管理する機能を追加してみましょう。

最初に、Triglav の root ディレクトリに移動し、master ブランチを最新の状態にします。

$ git checkout master
$ git pull origin master

次に、ミドルウェア管理機能追加用のブランチを作成し、そのブランチに移動します。

$ git checkout -b manage-middleware

scaffold によるコード自動生成

Rails には scaffold という、自動でコード生成してくれる機能がありますので、まずはこれを利用して、必要なファイルを自動生成してみます。ミドルウェアは、名前とバージョンを管理するものとします。

$ bundle exec rails generate scaffold middleware name:string version:string
      invoke  active_record
      create    db/migrate/20121129035419_create_middlewares.rb
      create    app/models/middleware.rb
      invoke    rspec
      create      spec/models/middleware_spec.rb
      invoke  resource_route
       route    resources :middlewares
      invoke  scaffold_controller
      create    app/controllers/middlewares_controller.rb
      invoke    erb
      create      app/views/middlewares
      create      app/views/middlewares/index.html.erb
      create      app/views/middlewares/edit.html.erb
      create      app/views/middlewares/show.html.erb
      create      app/views/middlewares/new.html.erb
      create      app/views/middlewares/_form.html.erb
      invoke    rspec
      create      spec/controllers/middlewares_controller_spec.rb
      create      spec/views/middlewares/edit.html.erb_spec.rb
      create      spec/views/middlewares/index.html.erb_spec.rb
      create      spec/views/middlewares/new.html.erb_spec.rb
      create      spec/views/middlewares/show.html.erb_spec.rb
      create      spec/routing/middlewares_routing_spec.rb
      invoke      rspec
      create        spec/requests/middlewares_spec.rb
      invoke    helper
      create      app/helpers/middlewares_helper.rb
      invoke      rspec
      create        spec/helpers/middlewares_helper_spec.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/middlewares.js.coffee
      invoke    scss
      create      app/assets/stylesheets/middlewares.css.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.css.scss

こんな形で、たくさんファイルが生成されます。

コマンドでは middleware と単数系で指定しましたが、生成されたファイルを見ると、middleware が単数系のものもあれば複数形のものもあります。どのケースで単数系が使われ、どのケースで複数形が使われるかは、Rails の規約で決まっています。

データベースマイグレーション

生成されたファイルの中に、db/migrate/20121129035419_create_middlewares.rb というファイルがあります。(数字の部分は実行した日時によって変わります。)これは、ミドルウェア管理用のテーブルをデータベースに追加するためのファイルで、内容は以下のようになっています。

class CreateMiddlewares < ActiveRecord::Migration
  def change
    create_table :middlewares do |t|
      t.string :name
      t.string :version

      t.timestamps
    end
  end
end

テーブル名は Rails の規約に従い、複数形になっています。

rake db:migrate を実行することで、テーブルが作成されます。

$ bundle exec rake db:migrate
==  CreateMiddlewares: migrating ==============================================
-- create_table(:middlewares)
   -> 0.4230s
==  CreateMiddlewares: migrated (0.4231s) =====================================

作成されたテーブルを見てみます。

$ mysql -uroot triglav_development
mysql> show tables;
+-------------------------------+
| Tables_in_triglav_development |
+-------------------------------+
| activities                    |
| comments                      |
| host_relations                |
| hosts                         |
| middlewares                   |
| roles                         |
| schema_migrations             |
| services                      |
| users                         |
+-------------------------------+
9 rows in set (0.00 sec)

mysql> desc middlewares;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| version    | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

指定された、name と version が存在しています。id, created_at, updated_at は自動で追加されます。

動作確認

他のファイルの内容を見る前に、http://yourhost:3000/middlewares にアクセスして、ミドルウェアの追加、編集、削除などができることを確認してみましょう。

ルーティングの確認

git diff でルーティング設定がどのように変わったかを見てみましょう。

$ git diff config/routes.rb
diff --git a/config/routes.rb b/config/routes.rb
index 28ef6f9..0abc8c3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,6 @@
 Triglav::Application.routes.draw do
+  resources :middlewares
+
   root 'root#index'
   get '/caveat', to: 'root#caveat'

resources :middlewares という一行が追加されています。これにより、以下のようなルーティングが設定されます。

$ rake routes |grep middleware
            middlewares GET    /middlewares(.:format)                               middlewares#index
                        POST   /middlewares(.:format)                               middlewares#create
         new_middleware GET    /middlewares/new(.:format)                           middlewares#new
        edit_middleware GET    /middlewares/:id/edit(.:format)                      middlewares#edit
             middleware GET    /middlewares/:id(.:format)                           middlewares#show
                        PATCH  /middlewares/:id(.:format)                           middlewares#update
                        PUT    /middlewares/:id(.:format)                           middlewares#update
                        DELETE /middlewares/:id(.:format)                           middlewares#destroy

コントローラの内容確認

app/controllers/middlewares_controller.rb の内容を見てみましょう。

class MiddlewaresController < ApplicationController
  # GET /middlewares
  # GET /middlewares.json
  def index
    @middlewares = Middleware.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @middlewares }
    end
  end

  # GET /middlewares/1
  # GET /middlewares/1.json
  def show
    @middleware = Middleware.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @middleware }
    end
  end

  ... 以下略 ...

index, show, new, edit, create, update,destroy といったアクションが自動生成されていることが確認できます。

モデルの内容確認

app/models/middleware.rb の内容を確認してみましょう。

class Middleware < ActiveRecord::Base
end

これだけです。

ビューの内容確認

app/views/middlewares の下に、複数のビューファイルが生成されています。

$ ls app/views/middlewares
_form.html.erb  edit.html.erb  index.html.erb  new.html.erb  show.html.erb

M, V, C それぞれのファイルの内容に関する説明は、ハンズオンの場でディスプレイに映しながら説明します。

コミット

最後に、manage-middleware ブランチにコミットして、機能追加開発を終えましょう。

$ git add .
$ git commit

まとめ

今回は MVC とは何かについて軽く学び、Rails の scaffold を用いてコードを自動生成してみました。

追加した機能はその中だけで閉じていて、Triglav の他の部分とつながっていないため、機能追加した、という実感はあまりないかもしれませんが、Rails で開発を行うためには、M, V, C それぞれのファイルをどの位置にどのように配置するのか、といったことは掴んでもらえたかと思います。

また、scaffold は、やりたいことによっては、不要なファイルやアクションも追加されてしまうので、Rails での開発に慣れてくると、scaffold は使わずに、自前でファイルを作ることが多いです。

とはいえ、scaffold で自動生成されるファイルを見ることによって、Rails で開発する上でのルールがわかるので、慣れないうちは scaffold を大いに活用してください。特に、単数形と複数形の使い分けは覚えにくいので、忘れたら scaffold で適当にファイルを生成してみましょう。

単に確認のために作成し、確認後に元の状態に戻したい場合には、以下のように git コマンドを実行することで、生成されたファイルやディレクトリを削除し、変更されたファイルを元に戻すことができます。

$ git clean -nd
Would remove app/assets/javascripts/middlewares.js.coffee
Would remove app/assets/stylesheets/middlewares.css.scss
Would remove app/controllers/middlewares_controller.rb
Would remove app/helpers/middlewares_helper.rb
Would remove app/models/middleware.rb
Would remove app/views/middlewares/
Would remove db/migrate/20121129065540_create_middlewares.rb
Would remove spec/controllers/
Would remove spec/helpers/middlewares_helper_spec.rb
Would remove spec/models/middleware_spec.rb
Would remove spec/requests/
Would remove spec/routing/
Would remove spec/views/

$ git clean -fd
Removing app/assets/javascripts/middlewares.js.coffee
Removing app/assets/stylesheets/middlewares.css.scss
Removing app/controllers/middlewares_controller.rb
Removing app/helpers/middlewares_helper.rb
Removing app/models/middleware.rb
Removing app/views/middlewares/
Removing db/migrate/20121129065540_create_middlewares.rb
Removing spec/controllers/
Removing spec/helpers/middlewares_helper_spec.rb
Removing spec/models/middleware_spec.rb
Removing spec/requests/
Removing spec/routing/
Removing spec/views/

$ git checkout .