Programming log - Shindo200

イベント参加記録とプログラミング系の雑記

パーフェクト Rails を読みました(2章)

この記事は『パーフェクト Rails を読みました』シリーズの2回目の記事です。
1回目の記事は『パーフェクト Rails を読みました(1章) - Programming log - Shindo200』です。

午前中から引き続き、『パーフェクト Ruby on Rails』の2章を読み終えましたので、記録を書き残しておきます。

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails

2章の感想

2章では ActiveRecordActionController などの使い方を学んでいきます。まだまだ入門編という感じでしょうか。Rails 4.1 で加わった機能についてはあまり知りませんでしたので、読んでいて勉強になりました。

備忘録

AvtiveRecord::Relation

モデルに対して where などのメソッドを実行すると、ActiveRecord::Relation というクラスのオブジェクトが返ってきます。

$ ./bin/rails c
irb(main):001:0> Book.where("price > 2000").class
=> Book::ActiveRecord_Relation

ActiveRecord::Relation のオブジェクトは「どんなSQLを発行するのか」という情報を内部に持っているそうです。APIドキュメントを見た感じでは、values メソッドを使って情報を見ることができそうでした。

# そのまま出力すると見難いので、pp を使っています
irb(main):002:0> pp Book.where("price > 2000").select(:name).order(:price).limit(5)
{:where=>["price > 2000"],
 :select=>[:name],
 :order=>
  [#<Arel::Nodes::Ascending:0x007fc8f07c3ab0
    @expr=
     #<struct Arel::Attributes::Attribute
      relation=
       #<Arel::Table:0x007fc8ee8bedb8
        @aliases=[],
        @columns=nil,
        @engine=
         Book(id: integer, name: string, published_on: date, price: integer, number_of_page: integer, created_at: datetime, updated_at: datetime),
        @name="books",
        @primary_key=nil,
        @table_alias=nil>,
      name=:price>>],
 :limit=>2}

to_sql メソッドで発行される SQL を確認できます。

irb(main):002:0> Book.where("price > 2000").select(:name).order(:price).limit(5).to_sql
=> "SELECT  \"books\".\"name\" FROM \"books\"  WHERE (price > 2000)  ORDER BY \"books\".\"price\" ASC LIMIT 2"

ちなみに、上記のように SQL を組み立てるメソッドをチェーンさせることを Query Interface と呼ぶらしいです。

default_scope は使いどころに注意

P.51 「scope を定義する」から引用します。

しかし、default_scope を見落として思わぬ挙動をさせてしまうこともあります。特に集団で開発する場合などには、使いどころに注意が必要です。

以前、どこかで聞いたようなお話……

独自のバリデーションを作る

Rails には様々なバリデーションメソッドが組み込まれています。

組み込みないようなバリデーションを行いたいときや、メッセージをカスタマイズしたいときは、以下のように validate ブロック内にバリデーションを行う処理を書けば、独自のバリデージョンを作ることができます。

class Book < ActiveRecord::Base
  # name に「foo」という文字列が含まれている場合は、バリデーションエラーとする
  validate do |book|
    book.errors[:name] << "I don't like foo." if book.name.include? 'foo'
  end
end

APIドキュメントでは、以下のような書き方が紹介されていました。

class Book < ActiveRecord::Base
  # name に「foo」という文字列が含まれている場合は、バリデーションエラーとする
  validate :must_not_include_foo

  def must_not_include_foo
    book.errors[:name] << "I don't like foo." if book.name.include? 'foo'
  end
end

複雑なバリデーションの作り方については、9章でより詳しく解説されるようです。楽しみですね。

ActiveRecord enums

ActiveRecord enumsRails 4.1 で新たに加わった機能です。

例えば、ユーザがステータスという属性を持っていて、ステータスが 1 ならば仮登録、2 ならば本登録、3 ならば 退会済みの状態だとします。仮登録でユーザを作りたいときは、以下の処理を実行します。

User.create name: 'foo', status: 1

実行時に問題になることはないのですが、このコードでは「status: 1」がどのようなステータスを表しているのかがわかりません。困りました。ここで ActiveRecord enum を使うと、ステータスをわかりやすく書くことができます。User モデルを以下のように修正します。

# User モデル
class User < ActiveRecord::Base
  enum status: %w(temporary active resign)

   # ...
end

これで ActiveRecord enum による実装はできあがりです。rails console を使っていろいろと試してみます。

irb(main):001:0> User.create name: 'foo', status: 'temporary'
=> #<User id: 1, name: "foo", status: 1, created_at: "2014-06-04 08:40:54", updated_at: "2014-06-04 08:40:54">
irb(main):002:0> User.create name: 'foo', status: :active
=> #<User id: 2, name: "foo", status: 2, created_at: "2014-06-04 08:40:54", updated_at: "2014-06-04 08:40:54">
irb(main):003:0> User.create name: 'foo', status: 3
=> #<User id: 3, name: "foo", status: 3, created_at: "2014-06-04 08:40:54", updated_at: "2014-06-04 08:40:54">

status に文字列、シンボル、数値を渡してみましたが、どれも数値で登録されていますね。ステータスを文字で保存しておくより数値で保存しておく方が空間効率はよいので、enum 型を使っていきたいですね。

さらに、ActiveRecord enum は以下のような述語メソッドを提供してくれます。

irb(main):001:0> user = User.find(1)
irb(main):002:0> user.temporary?
=> true
irb(main):003:0> user.active?
=> false
irb(main):004:0> user.resign?
=> false

これは便利そうですね。

エラーになってしまうコード

P.58 リスト2.9 の一部分を抜き出します。

class Book < ActiveRecord::Base
  # ...

  # あるいはメソッドを使って以下のようにも書ける
  before_validation :add_lovely_to_cat
  def add_lovely_to_cat
    book.name = book.name.gsub(/Cat/) do |matched|
      "lovely #{matched}"
    end
  end
end

「Book を保存する前に、名前に"Cat"が含まれていたら、"lovely Cat"という文字に置き換える」という処理が書かれているのですが、add_lovely_to_cat メソッドの変数 book が未定義なので、NameError になってしまいました。たぶん、変数 book ではなく self を使えばいいと思うのですが、どうなのでしょうか。

class Book < ActiveRecord::Base
  # ...

  before_validation :add_lovely_to_cat
  def add_lovely_to_cat
    self.name = self.name.gsub(/Cat/) do |matched|
      "lovely #{matched}"
    end
  end
end

お疲れ様でした

これで Part1「Rails ~ overview」が終わりました。Part2 「実践テクニック」からが本番といった感じでしょうか。