早く知ってたら良かったrailsの技

  • 855
    Like
  • 0
    Comment
More than 1 year has passed since last update.

はじめに

自分が rails をさわり始めたときはバージョン1からバージョン2に変わるあたりだったのですが、バージョン2が出た年を振り返るとなんと2007年でした。
月日の流れが速い事に驚く中、早く知ってたら良かったのになぁって事をつらつらとまとめてみました。
最近 rails さわり始めてみたよ!って方の参考になれば良いなと思います。
今回は便利な gem とかではなく、素のrailsで出来ることを挙げています。
ちなみにバージョンは以下の環境です。

About your application's environment
Ruby version              2.1.3-p242 (x86_64-darwin14.0)
RubyGems version          2.2.2
Rack version              1.5
Rails version             4.1.8

app 以下のディレクトリ構成は追加しても良いんだよ

rails new したときの app 以下のディレクトリ構成は以下のようになっています。

- app/
 |+ assets/
 |+ controllers/
 |+ helpers/
 |+ mailers/
 |+ models/
 |+ views/

最初はこの通りに作らなきゃいけないのかなって思っていたのですが、最初から用意されている枠に合わない物は追加しても大丈夫です。
例えば自作のバリデーターをまとめる validators とか、バックグラウンドで動くワーカーが入ってる workers とかですね。

- app/                         
 |+ assets/
 |+ controllers/
 |+ decorators/
 |+ helpers/
 |+ jobs/
 |+ mailers/
 |+ models/
 |+ queries/
 |+ uploaders/
 |+ validators/
 |+ views/
 |+ workers/

それでも、app 以下じゃ無いよなーって思うような、汎用的なユーティリティとかは lib 以下に配置することもあります。
ちょっと気をつけるのがロードパス。
app 以下の一階層分は勝手に呼んでくれるのですが、例えば app/workers/concerns なんてディレクトリを作ったとすると、そこまでは読んでくれません。
なので config/application.rb でロードするように設定しましょう。
下の例では式展開を使っているので %w では無く、大文字の %W であることにも注意です。
深い階層まで一気に追加する場合は、「lib 以下もロードパスに追加」で使っている方法も使えますよ。

require File.expand_path('../boot', __FILE__)
require 'rails/all'
Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    # app 以下の独自ディレクトリも読み込む
    config.autoload_paths += %W(#{config.root}/app/jobs/concerns #{config.root}/app/workers/concerns)
    # lib 以下もロードパスに追加
    config.autoload_paths += Dir["#{config.root}/lib/**/"]
    # 中略
  end
end

ネストしたリソースの routes は shallow を検討しよう

ここでは、説明のためにサンプルとなるアプリケーションを作ります。
ユーザー User は複数のノート Note を持っているアプリケーションをイメージしてください。

./bin/rails g scaffold user name:string
./bin/rails g scaffold note user:references title:string body:text

さて、リソースがネストしているので、config/routes.rb にもそのように記述します。

Rails.application.routes.draw do
  resources :users do
    resources :notes
  end
end

そうするとルーティングは以下のようになります。

        Prefix Verb   URI Pattern                              Controller#Action
    user_notes GET    /users/:user_id/notes(.:format)          notes#index
               POST   /users/:user_id/notes(.:format)          notes#create
 new_user_note GET    /users/:user_id/notes/new(.:format)      notes#new
edit_user_note GET    /users/:user_id/notes/:id/edit(.:format) notes#edit
     user_note GET    /users/:user_id/notes/:id(.:format)      notes#show
               PATCH  /users/:user_id/notes/:id(.:format)      notes#update
               PUT    /users/:user_id/notes/:id(.:format)      notes#update
               DELETE /users/:user_id/notes/:id(.:format)      notes#destroy
         users GET    /users(.:format)                         users#index
               POST   /users(.:format)                         users#create
      new_user GET    /users/new(.:format)                     users#new
     edit_user GET    /users/:id/edit(.:format)                users#edit
          user GET    /users/:id(.:format)                     users#show
               PATCH  /users/:id(.:format)                     users#update
               PUT    /users/:id(.:format)                     users#update
               DELETE /users/:id(.:format)                     users#destroy

これでルーティングもネストしたリソースに沿った物になりました。
ただ、特定のノートを参照しようとしたとき、ノートIDさえ分かれば対象は一意に決まるはずですが、その場合でもユーザーIDが必要になってしまいます。
そこで、shallow: true を指定すると、、、

Rails.application.routes.draw do
  resources :users, shallow: true do
    resources :notes
  end
end
       Prefix Verb   URI Pattern                         Controller#Action
   user_notes GET    /users/:user_id/notes(.:format)     notes#index
              POST   /users/:user_id/notes(.:format)     notes#create
new_user_note GET    /users/:user_id/notes/new(.:format) notes#new
    edit_note GET    /notes/:id/edit(.:format)           notes#edit
         note GET    /notes/:id(.:format)                notes#show
              PATCH  /notes/:id(.:format)                notes#update
              PUT    /notes/:id(.:format)                notes#update
              DELETE /notes/:id(.:format)                notes#destroy
        users GET    /users(.:format)                    users#index
              POST   /users(.:format)                    users#create
     new_user GET    /users/new(.:format)                users#new
    edit_user GET    /users/:id/edit(.:format)           users#edit
         user GET    /users/:id(.:format)                users#show
              PATCH  /users/:id(.:format)                users#update
              PUT    /users/:id(.:format)                users#update
              DELETE /users/:id(.:format)                users#destroy

これで notes#show の時に冗長だったユーザーID指定が不要になりました。
ちょっと注意なのが form_for に渡すパスです。
例えば、notes#create の時は /users/:user_id/notes で、notes#update の時は /notes/:id とユーザーIDの有無が異なります。
これだと form_for(@note) do のままだと notes#new の時に undefined method notes_path なんて言われちゃいます。
shallow オプションを指定しない場合は form_for([@user, @note]) のように指定すれば良い)
そこで、form_for は以下のように設定します。

form_for(@note, url: polymorphic_path([@user, @note])) do |f|

ちなみにコントローラーの new, edit はこんな感じ

# GET /notes/new
def new
  @user = User.find params[:user_id]
  @note = @user.notes.build
end

# GET /notes/1/edit
def edit
end

こうすることで、
新規作成時には action="/users/1/notes"、編集時には action="/notes/1" とちゃんと指定できます。

nilかも?って時には ||, && とか presense とか try とか便利だよ

例えば、User クラスには名前 name という変数があるとき、namenil だったら値を設定したいなんて時は、

user.name = 'anonymous' unless user.name

こうせずに

user.name ||= 'anonymous'

こんな感じで書くことが出来るんですね。
ruby だとこれを「nilガード」なんて呼ばれています。
分かりやすく式を展開すれば下の意味と同じです。

user.name = (user.name || 'anonymous')

もちろん使いどころは nilガードだけじゃありません。
User クラスにニックネーム nickname もあったとして、
「ニックネームが設定されていればそちらを、されていない(nilなら)名前を出したい」なんて時はこうすれば良いわけです。

user.nickname || user.name

同じようなユースケースで presence が使える場合もあります。
presencepresent? メソッドが真なら self を、偽なら nil を返すメソッドです。
present?nil, false, 空の配列, 空のハッシュ, 空の文字列, 特定文字列のみ文字列を判定してくれるメソッドです。
ここでの特定文字列とは 正規表現 /\A[[:space:]]*\z/ で表される物です。

user.namenil だけじゃ無くて空白文字も判定したいんだって時に

user.name.present? ? user.name : 'anonymous'

presence を使うと

user.name.presence || 'anonymous'

と書けるわけですね。
次に、少し例を変えて UserNotehas_many で持っているとしましょう。
こんな感じです。

class User < ActiveRecord::Base
  has_many :notes
end

class Note < ActiveRecord::Base
  belongs_to :user
end

さて、Note からユーザー名を表示したいとして、でも必ずしも対応するユーザーが存在するか分からないとき、

note.user.name if note.user

とか

note.user && note.user.name

みたいに書くことも出来るのですが、こんな時には try が使えます。
try は引数でメソッド名を渡して実行するもので、ただし対象がnil の時には実行されずに nil を返してくれる物です。
実装はシンプルで、ObjectNilClasstry が用意されているんですね。

class Object
  def try(*a, &b)
    if a.empty? && block_given?
      yield self
    else
      public_send(*a, &b) if respond_to?(a.first)
    end
  end
end

class NilClass
  def try(*args)
    nil
  end
end

try だとメソッド名の打ち間違いなども nil で返ってきますが、
存在しないメソッドの場合は NoMethodError を返してくれる try! も用意されています。

delegate も使ってみよう

上の例では note.user.name とメソッドの呼び出しをしていますが、
オブジェクト指向プログラミングの界隈だと、「デメテルの法則」に反しているとも言われます。

それじゃあ、といって

class Note < ActiveRecord::Base
  belongs_to :user

  def user_name
    user.try :name
  end
end

ノートからユーザー名を取得するメソッドを定義しても良いのですが、数が多くなると大変になっちゃいますね。
そんなときには delegate が使えます。

class Note < ActiveRecord::Base
  belongs_to :user
  delegate :name, to: :user, prefix: :author, allow_nil: true
end

こうすると note.author_name が呼び出せるようになります。
delegate の使い方は、委譲するメソッド名、移譲先(to)、さらにはメソッドのプレフィックス(prefix)、そしてnilを許可するかどうか(allow_nil)です。
prefix: false なら note.name と呼び出すことになりますし、prefix: true なら note.user_name となります。
allow_nil: true としておけば note.usernil の場合でもエラーにならないので便利ですね。
さらに、移譲先は関連以外でも定数、クラス変数、インスタンス変数でも大丈夫で、多段の delegate をすることも可能です。

class Foo
  CONSTANT
  @@class_val

  def initialize
    @instance_val
  end

  delegate :foo, to: :CONSTANT
  delegate :bar, to: :@@class_val
  delegate :baz, to: :@instance_val
end

ちなみにプレーンなruby にも委譲の為の Forwardable モジュールが用意されており、使い方も似ています。
Forwardable の方はメソッドのリネームが出来る所が長所ですが、
ActiveSupportdelegate に用意されている prefixallow_nil が使い勝手が良いので、rails だと ActiveSupportdelegate をよく使います。

クラスマクロは自分で作れる

クラスマクロって例えば attr_accessor みたいなやつですね。

class User
  attr_accessor :name
end

これは用意されている物を使うだけで無く、自分で作ることも出来るんです。
「マクロとか難しそう」という訳では無く、その実態はただのクラスメソッドなので怖くないよ。

class User
  attr_accessor :name
  def self.suffix attr, value
    define_method("#{attr}_with_suffix") {
      name = instance_variable_get "@#{attr}"
      "#{name} #{value}" if name.present?
    }
    alias_method_chain attr, :suffix
  end
end

class Boy < User
  suffix :name, 'くん'
end

class Girl < User
  suffix :name, 'さん'
end

オマケで alias_method_chain の例も入れてみたよ。
クラスマクロは def self.suffix attr, value の部分で、この定義によって継承したクラスでクラスマクロが使えるようになっている訳です。

> girl = Girl.new
=> #<Girl:0x007fb9314610f8>
> girl.name = 'はなこ'
=> "はなこ"

> girl.name_with_suffix
=> "はなこ さん"
> girl.name
=> "はなこ さん"
> girl.name_without_suffix
=> "はなこ"

実行してみた結果が上の様子です。> が入力、=> が出力です。
クラスマクロによって、使うと "#{attr}_with_suffix" というメソッドが作られるようになっています。
つまり Boy, Girl クラスで suffix :name, 'さん' と使ったことによって、
name_with_suffix メソッドがマクロによって定義されているのです。
さらにマクロによって定義したメソッドの内容は、「名前が設定されていれば、後ろにマクロで指定した文字列を追加する」という物だったので、girl.name_with_suffix によって「はなこ さん」と返ってきている訳です。
この例だと実は girl.name と呼び出すだけで girl.name_with_suffix と同じ結果を得ることが出来ます。
これは、alias_method_chain によって、元々の name メソッドが name_without_suffix として待避され、name_with_suffix として定義したメソッドが name としても呼び出せるようになっているからなのです。

クラスマクロを定義する場所は、今回のように継承元クラスや、
ActiveSupport::Concern でモジュール化するなど、いろいろな方法があります。

super do で操作を差し込む

さて、下のようなクラスを作ってみます。

class User
  attr_accessor :age

  def initialize
    @age = 0
  end

  def birthday
    puts 'happy birthday!'
    @age += 1
  end
end

User#birthday メソッドは、「happy birthday!」とメッセージを表示し、
年齢に1を加えてその値を返す物です。

class Boy < User
  def birthday
    super
    puts 'some process...'
  end
end

Userクラスを継承したBoyクラスにおいて、birthdayメソッドをオーバーライドしようとしたとき、
super によって継承元のメソッドを呼び出すことが可能です。
しかし、「メッセージの表示」と「年齢の加算」の間に処理を挟みたい時にはどうしましょう。
そのために super を使わずにオーバーライドしてしまうと、メッセージの定義が重複するなどしてメンテナンス性が下がってしまいます。
そんなときには継承元クラスに処理を差し込めるポイントを作っておきましょう。

class User
  attr_accessor :age

  def initialize
    @age = 0
  end

  def birthday
    puts 'happy birthday!'
    yield if block_given?
    @age += 1
  end
end

class Boy < User
  def birthday
    super do
      puts 'some process...'
    end
  end
end

Userクラスに yield if block_given? を用意したので、
継承したクラスにおいて birthday メソッドをオーバーライドしたとき、
ブロックによる super を呼び出したときに、そこに処理を差し込むことが可能になります。

ログイン処理などによく用いられる plataformatec/devise では、
例えば Devise::SessionsController 等において、「ログインしたときにそのユーザーに何か処理したいな」ってケースのために処理差し込みのポイントを用意してくれていたりします。

class Devise::SessionsController < DeviseController
  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message(:notice, :signed_in) if is_flashing_format?
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end
end

active model は使えるやつ

rails アプリを作成していると、DBには永続化しないんだけど、ActiveRecordと同じような使い勝手のモデルが欲しいなーと思うことがあります。
例えば検索用のフォームなどですね。
view で form_for 使いたいし、validate も同じように定義したいからです。

そんなときには ActiveModel の出番です。

class UserSearchForm
  include ActiveModel::Model

  attr_accessor :name, :age
  validates :name, presence: true
end

使い方もとても簡単。include ActiveModel::Model するだけです。
これで validates なども使えるようになっています。

おわりに

便利そうな Rails Tips をまとめてみました。誰かのお役に立てば幸いです。
他にも思いついたら加筆していきたいと思いますし、
こんなのも早く知ってたら良かった!と思った物があれば是非教えてくださいな。