Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1384 articles
Browse latest View live

Rails 4でcallback(before/after/around action)の実行順序おさらい

$
0
0

before_action(旧 before_filter)はchainで複数指定することができます。
before_actionを複数指定したりaround_actionと組み合わせたりした場合の実行順序について確認しておきましょう。

以下のようにWorksControllerを定義しました。

class WorksController < ApplicationController
  around_action :around_works_1
  before_action :before_works_1, :before_works_2
  after_action :after_works_1
  after_action :after_works_2
  around_action :around_works_2
  prepend_before_action :prepend_before_works
  prepend_after_action :prepend_after_works
  prepend_around_action :prepend_around_works

  # callback定義はログを出すだけ

ApplicationControllerにはこのように記載してあります。

class ApplicationController < ActionController::Base
  around_action :around_app_1
  before_action :before_app
  after_action :after_app
  around_action :around_app_2
  prepend_around_action :prepend_around_app
  prepend_before_action :prepend_before_app
  prepend_after_action :prepend_after_app

  # callback定義はログを出すだけ

この状態でworks#indexを表示すると、以下のような結果になります。

prepend_around_works start
prepend_before_works
prepend_before_app
prepend_around_app start
around_app_1 start
before_app
around_app_2 start
around_works_1 start
before_works_1
before_works_2
around_works_2 start
  Rendered text template (0.1ms)
around_works_2 end
after_works_2
after_works_1
around_works_1 end
around_app_2 end
after_app
around_app_1 end
prepend_around_app end
prepend_after_app
prepend_after_works
prepend_around_works end

CallbackChainは単なる配列なので、記述するときも配列を常にイメージするとわかりやすいです。

callback定義時には以下のように配列に入っていきます。

before_action
beforeのcallback chainにappendされる
after_action
afterのcallback chainにappendされる
around_action
beforeとafterのcallback chainにそれぞれappendされる
prepend_before_filter
beforeのcallback chainにprependされる
prepend_after_filter
afterのchainにprependされる
prepend_around_filter
beforeとafterのchainにそれぞれprependされる

もちろん、継承関係にあるときは先に親クラスをロードするので、そちらが先に評価されます。

あとは、実行時に「beforeのchainを先頭からすべて実行 → actionを実行 → afterのchainを末尾からすべて実行」になると考えれば簡単です。

※Chainが途中で中断する仕組みは、Railsでbefore_filter/before_actionがアクションを中止する仕組みを読んでみるの記事で記載しています。


Railsで検索を高速化するならこれで決まり!Sunspotで始めるSolr入門

$
0
0

Railsで簡単に使える全文検索と言えば、Sunspotが人気です。

非常に使いやすくできていて、わずか数ステップで本格的な全文検索を始めることができます。
バックエンドは信頼と実績のApache Solrなので、性能・信頼性・拡張性は折り紙付きです。

そんなSunspotですが、実際にサーバに設置してサービス運用に乗せるとなると、ローカルで試しに動かす分には必要なかったことも気にする必要がでてきます。

  • データのバックアップはどうすれば良いんだ?
  • 公開したあとにインデックス張り直すときは?
  • この1000行以上ある設定ファイルはいったい何なんだ?

最近は、Railsシステムの高速化・パフォーマンスチューニングをさせて頂くことも増えたので、Sunspot/Solrも小規模ながら活用できるようになってきました。
これを機に社内勉強会を開催したので、そのスライドをSlideShareにアップロードしておきました。

目次

  • はじめに
  • Sunspotを使ってみる
  • どのくらい速い?
  • Sunspotの動作モデル
  • Solrの概要
  • Solrの管理画面
  • Solrで直接検索してみる
  • Solrの設定
  • Sunspot Tips

まとめ

Solrの動作の概要を理解して、小規模なら運用に乗せられる・調べながら設定変更やチューニングができるようになることを目指して勉強会を開催しました。
特にschema.xmlのあたりは理解すれば柔軟で便利ですが、初めてだととっつきにくいため、この部分が好評でした。

機会があれば、1台ではとうてい捌けない規模のデータも扱ってみたいなあ。

[Rails 4.0] ActiveRecordのコールバックが呼ばれる順番まとめ

$
0
0

ActiveRecordにはコールバックがたくさんあります。
実行順序がよくわからなくなるので、Rails 4が出たことだし改めてまとめてみました。

※ActiveRecord::Callbacksのヘッダコメントに全部書いてあります

使うモデル

まずは、このようなクラスを作りました。

class Work < ActiveRecord::Base
  callbacks = %w(before after).product(%w(validation save create update destroy)).map{|a|a.join('_')}
  callbacks += %w(after_commit after_rollback after_initialize)
  callbacks.each do |callback|
    send(callback) { logger.debug callback }
  end 

  validates :name, presence: true

  def initialize(*)
    logger.debug "initialize"
    super
  end
end

create/updateしてみる

まずは手始めにcraete/updateしてみます。validation,saveなどのコールバックが呼ばれます。

### newするとinitializeが呼ばれる
work = Work.new(name: 'hoge')
# initialize
# after_initialize

### saveするとvalidation, save, create, commitが呼ばれる
work.save!
# -- BEGIN TRANSACTION
# before_validation
# -- validate
# after_validation
# before_save
# before_create
# -- INSERT INTO "works" ...
# after_create
# after_save
# -- COMMIT
# after_commit

### updateするとvalidation, save, update, commitが呼ばれる
work.update name: 'piyo'
# -- BEGIN TRANSACTION
# before_validation
# -- validate
# after_validation
# before_save
# before_update
# -- UPDATE "works" SET ...
# after_update
# after_save
# -- COMMIT
# after_commit

### validateに失敗するとrollbackが呼ばれる
work.update name: nil
# -- BEGIN TRANSACTION
# before_validation
# -- validate
# after_validation
# -- ROLLBACK
# after_rollback

### update_attributeではvalidateが実行されない
work.update_attribute :name, nil
# -- BEGIN TRANSACTION
# before_save
# before_update
# -- UPDATE "works" SET ...
# after_update
# after_save
# -- COMMIT
# after_commit

### update_columnではcallbackも何も呼ばれない
### ※serializeなども処理されないので注意
work.update_column :name, nil
# -- UPDATE "works" SET ...

find/destroyしてみる

create時だけでなく、findしたときもインスタンスは作られるのでinitializeが呼ばれます。
初期値を設定するときにafter_initializeを使う場合は注意しましょう。

また、destroyしたあともDBからデータが消えるだけでモデルオブジェクトは残るので、IDなど値は参照できます。

### findするとinitializeが呼ばれる
work = Work.first
# -- SELECT "works".* FROM ...
# initialize
# after_initialize

### pluckではインスタンスが作られず、callbackも呼ばれない
### ※serializeなどは処理される
Work.pluck(:name)
# --- SELECT "works".name FROM ...

### destroyするとdestroy, commitが呼ばれる
work.destroy
# -- BEGIN TRANSACTION
# before_destroy
# -- DELETE FROM "works" ...
# after_destroy
# -- COMMIT
# after_commit

コールバックでfalseを返してみる

before_saveやbefore_destroyなどでfalseを返すことで、ROLLBACKさせることができます。

### before_コールバックでfalseを返すとrollbackする
class Work
  before_save { logger.debug "before_save2"; false }
end
work = Work.first
work.save
# -- BEGIN TRANSACTION
# before_validation
# -- validate
# after_validation
# before_save
# before_save 2
# -- ROLLBACK
# after_rollback

これはvalidationをスキップするupdate_attributeもROLLBACKされます。<
update_columnはそもそもコールバックが呼ばれないので、そのまま保存されます。

まとめ

さんざん書かれていることですが、update/update_attribute/update_columnは、コールバック・validationの観点からは以下のような違いがあります。

update (旧update_attributes)
コールバックあり、validationあり
update_attribute
コールバックあり、validationなし
update_column
コールバックなし、validationなし

これらを意識し、安全で効果的にコールバックを活用したいですね。

ActiveRecordの便利機能previous_changes

$
0
0

ActiveRecordには便利なprevious_changesというメソッドがあります。
割と知名度が低いので、ここで使い方をご紹介します。

(08/26追記)
はてブでご指摘(?)頂きましたが、正確にはActiveRecordではなくActiveModel::Dirtyの機能です。

まずはchanges

previous_changesの前に基本のchangesです。
これは、saveする前に変更されたattributeを調べる機能です。

まずはfindした直後です。changed?は当然falseになります。

User.create(name: "Taro")
user = User.find(1)

user.changed?
# => false

user.changed
[]

user.changes
# => {}

user.changed_attributes
# => {}

attributeを書き換えてみましょう。changesが取得できるようになりました。

user.name = "Jiro"

user.changed?
# => true

user.changed
# => ["name"]

user.changes
# => {"name" => ["Taro", "Jiro"]}

user.changed_attributes
# => {"name" => "Taro"}

user.name_changed?
# => true

user.created_at_changed?
# => false

saveすると、changed?はまたfalseに戻ります。

user.save

user.changed?
# => false

ここでprevious_changes

ここで、さっきのchangesが欲しい!ということはよくありますよね。
典型的な例としては、after_save以降のタイミングで変更履歴を残したいなどがあげられます。

こんなときに、previous_changesが使えます。

user.previous_changes
# => {"name" => ["Taro", "Jiro"], "updated_at" => [Wed, 07 Aug 2013 02:15:34 UTC +00:00, Wed, 07 Aug 2013 02:24:55 UTC +00:00]}

シンプルで便利ですね。

なお、これはDBの履歴を持っているスーパーでワンダホーな機能ではなく、単にインスタンス変数として保存しているだけです。findした直後のインスタンスで取得したり、繰り返し呼んで古いデータを復元することはできません。念のため。

Devise3.0.2 でRails4 対応についてREADME に少し騙された話

$
0
0

Rails で認証系のGem で人気があるものといえばDevise かと思うのですが、Rails4 対応で少しはまったのでメモ。なお予め断っておきますと、これは2013年8月28日現在での問題であり今後すぐに問題なくなるかと思います。

Rails4 からStrongParameters が導入されましたが、Devise も同じくStrongParameters への対応が図られています。例えばusers テーブルを作ったとして、そこにDevise が必須としている以外のカラムを追加したいと思った場合、今まではモデルにattr_accessible を書いていたかと思いますが、今後StrongParameters でそのカラムを指定してやる必要があります。

このことについてはDevise のREADME の該当箇所にも書かれています。ここの解説によると「devise_parameter_sanitizer.for」を使え、とあります。

これは楽だなと思っていたのですが、しかし動かそうとしてみると何故かNoMethodError が。少し探してみるとIssue にこのようなものが。devise_parameter_sanitizer.for(:sign_up) << :something raises an error

え?RC 版でしかリリースしてないんですか?(´・ω・`; (注:現在正式版は3.0.2 が最新)

というわけで、上記メソッドを使いたくてもRC 版を使いたくない方は正式版がリリースされるまでもう少し待つ必要がある模様です。README にはもう書かれているのに…。

-追記-
3.0.2 のREADME はIssue で指摘を受けた後に訂正されていました。それと正確には「devise_parameter_sanitizer.for() <<」が未対応で、「devise_parameter_sanitizer.for() {}」については3.0.2 でも使用可能です。合わせて訂正いたします。

社内書籍管理システムを作ってみる その1

$
0
0

最近社内向けの書籍管理システムを空いた時間を見つけて開発したりしています。書籍の管理と言えばブクログさんとかが思い浮かぶと思います。おおよそ同じような機能が欲しくてちょっと再発明的な感じではありますが、そこに貸出機能が欲しかったり電子書籍端末等を扱いたいという社内の要望に細かく対応できることと、Ruby2.0 とRails4 に慣れるのを含めて開発しています。これから開発の流れを何回かに渡って軽い感じで書いていきたいと思います。

何をしたいか

いわゆる要件定義というものですね。目的がはっきりしていないと途中で何を作ってるのか分からなくなったり、出来上がったものが使いものにならないことなんてよくあることです。

今回のシステムには最低限で

  • 社内にある書籍を登録・管理し、検索できること
  • 書籍の貸出機能
  • 電子書籍も管理できること

が求められていました。

もちろんこの他にも色々追加機能要望が出てきましたが、最初はこれらの機能を実装する方向で動きました。最低限でもまずは動くものを作る、です。

どうやって実現するか

要件がまとまったら続いて設計ですね。上記の機能を実装するにあたって、まず大まかに必要なDB のテーブルを洗い出しました。

  • ユーザ
  • 書籍(電子書籍を含む)
  • 貸出明細
  • 電子書籍アカウント
  • 電子書籍端末

テーブルの解説

それぞれのテーブルと関係について軽く説明します。

ユーザ

ユーザはこのシステムの利用者です。本を追加したり、貸出申請をしたりするのに必要になります。権限レベルによって操作できる範囲を制限したりすることを想定していますが、それは次回以降にまとめたいと思います。

書籍

電子書籍を含む、本の情報を保持します。電子書籍かどうかはフラグを持たせて判断します。

貸出明細

貸出明細は1 回貸し出されるごとに生成されるものです。誰が何の本をいつ借りたかが分かるような情報を持ちます。

電子書籍アカウント

Kindle でのAmazon のアカウントをイメージしています。電子書籍であればいずれかの電子書籍アカウントに紐付いている必要があります。

電子書籍端末

電子書籍端末と電子書籍アカウントを分けたのはKindle を参考にして作成したためです。Kindle では1 つのアカウントで複数の端末を利用することが可能です。ただし全ての端末で同じ本がダウンロードされているというわけではありません。なので端末ごとに今ある電子書籍の情報を持つようにしました。サービスによってはここは仕組みが違うみたいなので、仕様を変更した方が良い場面も出てくるかもしれません。

図にまとめてみた

図にまとめるとどうやって作れば良いか、今作っているものは間違っていないか等よりわかりやすくなったりします。
今回のシステムは大体以下の図ような関係で構築することにしました。中間テーブルは必要に応じて作ります。

DB

上では述べませんでしたが、本を追加した際に誰が追加したかも一応保存するようにしました。何に使うかは考えていません。
少し設計で悩んだのは、電子書籍アカウントと電子書籍端末とで関連性を持たせなかった点です。これは電子書籍端末とアカウントの管理よりもまずは本の管理を重視した結果です。もちろん今後変更することはあり得ますので、あくまで最初に考えたものです。

その1 のまとめ

とりあえずデータの持ち方を大まかに決めたところまででひとまずその1 を締めたいと思います。上記設計を元にmigration やmodel を作ると、今度はcontroller の実装にとりかかることになります。実装についてはその2 で書きたいと思いますが、しかし実装では特に特別なことをしていないし、何を書けば良いのでしょうかね…悩みます(苦笑)
使ったgem とかまとめていけば良いのかな…何かコメント等頂いたらそれにお応えしたいと思います。

よくある?Rails失敗談 default_scope編

$
0
0

モデルからデータを取得する際に常に特定の検索条件を指定することができるdefault_scopeですが、
デメリットについてあまり注意を払わずに使ってしまって失敗しました。

サンプル事例

環境
Rails 3.2.12, 4.0
MySQL 5.1.65
※ 実際に問題が起きたバージョンが3.2.12 なのでそちらが中心になっています。

データを「名前」「年齢」どちらか指定された値で並び替えるという処理があったので、
特に指定が無ければid順で取得するという条件をdefault_scope を使って追加しました。

class User < ActiveRecord::Base
  default_scope -> { order(:id) }
end

これで、モデルからデータを取得する際に必ず ORDERが指定されるようになります。

User.all #=> SELECT "users".* FROM "users" ORDER BY id
User.where(name: "太郎") #=> SELECT "users".* FROM "users" WHERE "users"."name" = '太郎' ORDER BY id

失敗1 並び替えの条件が切り替えられない?

「名前」か「年齢」のどちらか指定された条件で並び替えたいので、以下のようなコードを書きました。

@users = case params[:ordering]
when "name"
  User.order(:name)
when "age"
  User.order(:age)
else
  User.all
end

しかし、この書き方では並び順に変化がありません。
実際に発行されているSQLを見てみます。

User.order(:name) #=> SELECT "users".* FROM "users" ORDER BY id,name

default_scopeで指定したORDERに追加する形で name での並び替えが指定されています。
先ずid順で並び替えてから、名前順で並び替えているので結局id順で並び替えたのと同じ結果になっています。
ORDERの指定を上書きしなくては期待した動作になりません。
そういった場合に使うのが reorder です。

User.reorder(:name) #=> SELECT "users".* FROM "users" ORDER BY name

idによる並び替えが消えてnameだけになりました。
これで期待通り名前順で結果が取得出来ました。

失敗2 他のテーブルとJOINしたらエラー

User単独で使っている間は問題なかったのですが、他のテーブルとJOINしてデータを取得した際にエラーが起きてしまいました。
具体的で良い例が思い浮かばなかったのですが。
UserテーブルにUserProfileテーブルをJOINして、UserProfileからデータを取得しようとする場合にエラーが起きます。

User.joins(:user_profile).select("user_profiles.location")
#=> Mysql2::Error: Column 'id' in order clause is ambiguous

idで並び替えようとしたけれど、idという列が複数あるので、どのidで並び替えたらいいのかわからないというエラーですね。
どのidを使うのかを指定します。

class User < ActiveRecord::Base
  default_scope -> { order("users.id") }
end

これでエラーは出なくなりました。
ちなみに、Rails4 では order(:id) という記述でも発行されるSQLでは
ORDER BY “users”.”id”
となるのでこの例で上げたようなエラーは起こりません。
ただし、order(“id DESC”) のように文字列で指定した場合はテーブル名が補完されることは無いので注意が必要です。

ORDER以外を指定した場合の問題

今回の例ではorderを指定した場合だけをとりあげましたが、default_scopeでは当然、whereやselectなど他のクエリメソッドをつかうことができます。
ただ、orderはreorderで上書きできるのですがそれ以外のメソッドでは、簡単にscopeを解除する方法が無いようです。(本当はあるのかもしれません)
scopeを解除する方法として思いつくのは、unscopedをつかって解除するというものです。

User.all #=> SELECT "users".* FROM "users" ORDER BY id
User.unscoped #=> SELECT "users".* FROM "users"

この方法は、default_scopeを設定したのが自分だけであればうまくいくと思います。
ですが、unscopedは acts_as_paranoid等のgemが指定しているscopeも解除してしまいます。
削除したはずのデータが取得できていることに気づかずに思わぬバグを生むことになりそうです。
gem等を使っていなくてもorderはそのままに、whereだけを解除するということも出来ないので気軽に使えるというわけでもないですね。

Rails4で使えるdefault_scope解除方法

Rails4からscopeを解除するunscope というメソッドが追加されたので、
他のscope解除メソッドとあわせてdefault_scopeも解除できるかどうか試してみました。
結論からいうと、Rails 4.0 でもdefault_scope を解除することができるのは reorder とunscoped だけでした。

reorder

orderだけにしか効きませんが、Rails4でもdefault_scope を解除する事が出来ました。

class User < ActiveRecord::Base
  default_scope { order(:id) }
end

User.all #=> SELECT "users".* FROM "users" ORDER BY "users"."id" ASC
User.reorder(nil) #=> SELECT "users".* FROM "users"

unscoped

こちらも動作に変わりはないようです。
scope default_scope どちらも解除可能ですが、細かい制御は出来ません。

class User < ActiveRecord::Base
  default_scope { order(:id) }
  default_scope { where(id: [1, 2, 3]) }
end

User.all #=> SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3) ORDER BY id ASC
User.unscoped #=> SELECT "users".* FROM "users"

except

指定した条件を削除することができます。

User.where("id < 10").order(:id).except(:order) #=> SELECT "users".* FROM "users" WHERE (id < 10)

個別に解除できて便利なのですが、default_scope は解除出来ません。

unscope

:where, :select, :group, :order, :lock, :limit,
:offset, :joins, :includes, :from, :readonly, :having
を削除可能です。
一見するとexceptと違いが無いようにも見えましたが、
whereを削除する場合のみHashとして渡すことで更に細かい指定が可能です。

User.where(id: [1,2,3]).order(:id).unscope(where: :id) #=> SELECT "users".* FROM "users" ORDER BY "users".id ASC

whereの条件に文字列を使っていた場合はエラーになります。

User.where("id < 10").order(:id).unscope(where: :id)  #=> RuntimeError: unscope(where: :id) failed: unscoping String is unimplemented. 

scope関連のメソッドは、引数がsymbolの場合と文字列の場合で動作が変わることが多いので注意が必要ですね。
exceptと同じように使う場合は文字列の条件でも問題ありません。

User.where("id < 10").order(:id).unscope(:where) #=> SELECT "users".* FROM "users" ORDER BY "users".id ASC

except よりも細かい制御が可能で良さそうなのですが、こちらもdefault_scopeは解除出来ませんでした。

感想

今回上げた例は単純なミスレベルですが、開発で実際に問題になる場合はもっと複雑なクエリを発行していることが多いのではないでしょうか?
特にJOINしたタイミングで初めて発覚するバグについてはテストがしっかりしていないと見逃してしまう可能性があるので厄介です。

default_scopeに関しては

  • 特定の条件でunscopedする必要がある場合は使わない
  • 一つのモデルに複数の条件を指定することは避ける
  • order以外を指定する場合は影響範囲を将来のことも含めて考えて不安があるなら使わない

という方針で暫くは行こうと思います。

Ruby on Rails 3.2.15がリリースされました –変更点まとめ

$
0
0

本日(日本時間2013年10月17日)、Ruby on Rails 3.2.15がリリースされました。3.2系最新リリースになります。
バグ修正の他、1件のセキュリティFIXが含まれています。3.2系ユーザは可能な限り早めにアップデートしましょう。

なお、予定ではこれが3.2系最終リリースで、これ以降はセキュリティFIXのみが提供されることになっています。
バグ修正はされない予定なので、アクティブなRailsプロジェクトは、早めにRails 4への移行を検討したほうが良さそうです。

セキュリティFIX

DoS攻撃につながる可能性のある脆弱性が修正されています。

ActionMailerのLogSubscriberに以下のようなコードが存在していまいしたが、

info("\nSent mail to #{recipients} (%1.fms)" % event.duration)

これはrecipientsに「test%d@example.com」のような文字列を渡すとArgumentErrorが発生します。
また、やり方は思いつきませんでしたが、仮に%1.fmsの部分を無視させることができれば「%10000d」のようにしてログに非常に長い文字列を出力させることが可能になります。
3.2.15では、この脆弱性が修正されました。

なお、本件のみ3.0, 3.1にも対応パッチがリリースされています。
https://groups.google.com/forum/#!topic/ruby-security-ann/yvlR1Vx44c8
4.0系は、この脆弱性は最初から存在しません。

ActionPackの変更点

意図しないIP spoofing attack

HTTPヘッダのCLIENT_IPとX_FORWARDED_FORに別の値をセットすることで、IP spoofing attackを狙う攻撃が存在します。
対策として、ActionDispatch::RemoteIp::GetIp#calculate_ipはこの2つの値が異なる場合に例外を発生させるようになっています。

しかし、一部X_FORWARDED_FORが空でCLIENT_IPのみをセットするプロキシが存在するため、その環境では通常アクセスでもIpSpoofAttackErrorがraiseされてしまっていました。
3.2.15ではこの問題が修正され、HTTP_CLIENT_IPとHTTP_X_FORWARDED_FORの両方がセットされている場合のみIP spoofing attackのチェックをするようになりました。

assert_recognizesのconstraints

assert_recognizesテストで、クエリ文字列にconstraintsを適用するとfailする問題が修正されました。

render partialのformat

以下のようなパターン(通常通りformat = :html)のとき、_myscript.js.erbと_form.html.erbがrenderされることが期待されます。

# app/controllers/users_controller.rb
def index
end

# app/views/users/index.html.erb
<%= render partial: 'myscript', formats: :js %>
<%= render 'form' %>

しかし、3.2.14では_form.js.erbがrenderされていました。
3.2.15ではこの問題が修正され、_form.html.erbがrenderされるようになりました。。

assert_redirected_toの第2引数

ActionDispatch::Assertions::ResponseAssertions#assert_redirected_toの第2引数には、テスト失敗時に表示するメッセージを渡せます。
これが表示されない不具合が修正されました。

ActiveRecordの変更点

inverse_ofその1

ActiveRecordにはinverse_ofという機能があります。

通常のassociationでは、relationを子→親と戻る方向に辿った場合、元のオブジェクトに変更は反映されません。

# app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name
  has_many :users
end

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company
end

# rails console
# 初期データ
User.create(name: '太郎', company: Company.create(name: '会社1'))

company = Company.first
compnay.name # => '会社1'
user = company.users.first
user.company.name = '会社2'
company.name # => '会社1'

inverse_ofをセットすると、これが反映されるようになります。

# app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name
  has_many :users, inverse_of: :company
end

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company, inverse_of: :users
end

# rails console
company = Company.first
compnay.name # => '会社1'
user = company.users.first
user.company.name = '会社2'
company.name # => '会社2'

この便利なinverse_ofですが、collection_proxyに対してfind_or_initialize_by_*で呼び出し、DBにレコードが存在しない場合に適用されないバグがありました。

# rails console
company = Company.first
compnay.name # => '会社1'
user = company.users.find_or_initialize_by_name('太郎') # DBに存在した
user.company.name = '会社2'
company.name # => '会社1' ★バグ!

3.2.15ではこの問題が修正され、「会社2」が取得できるようになりました。

※手元で確認したところ、この問題は4.0.0でも発生しました。あとuser = company.users.active.firstのようにscopeを付けるとやはり反映されないですが、これは正常なのかなあ。

inverse_ofその2

上記の例で、Userに以下のようなコールバックを追加します。companyを参照しているのがポイントです。
この状態で、company.usersにUserを追加します。

# app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :company
  belongs_to :company, inverse_of: :users

  after_save do
    puts "User created in company #{company.name}"
  end
end

# rails console
company = Company.first
company.users << User.new(name: '次郎')
# INSERT INTO "users" ("company_id", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 1], ["created_at", Thu, 17 Oct 2013 05:30:21 UTC +00:00], ["name", "じろう"], ["updated_at", Thu, 17 Oct 2013 05:30:21 UTC +00:00]]
# SELECT "companies".* FROM "companies" WHERE "companies"."id" = 1 LIMIT 1
# User created in company 会社1

after_saveのところで、CompanyをSELECTするSQLが走っています。
inverse_ofされているのだから、UserからCompanyは既にLOADされたものが見えるはずで、これは無駄です。

3.2.15では、この無駄なSELECTが実行されないようになりました。

引数付last(N)の発行するSQL

FinderMethods#lastは、Company.lastのように引数無しで実行するほかに、Company.last(5)のように最後から5個を配列で取得する使い方ができます。
このとき発行されるSQLが、以下のように変更されました。

Company.joins(:users).last(5)

# Rails 3.2.14
# => SELECT "companies".* FROM "companies" INNER JOIN "users" ON "users"."company_id" = "companies"."id" ORDER BY id DESC LIMIT 5

# Rails 3.2.15
# => SELECT "companies".* FROM "companies" INNER JOIN "users" ON "users"."company_id" = "companies"."id" ORDER BY "companies"."id" DESC LIMIT 5

3.2.14では、joinしたときにprimary keyが衝突することがありましたが、これが修正されました。

fixture読み込み

fixtures読み込みで、ディレクトリへのsymbolic linkが参照されるようになりました。

lock_versionを使うときのquote_value

lock_versionカラムを使うと簡単に楽観的ロックを実現できます。
このとき、内部的に使われるquote_valueに、カラムの型(この場合数値型であるべき)情報が渡されていませんでした。
これにより、一部の環境(JRuby + activerecord-jdbcmssql-adapter + SQLServer 2000など)で、不適切なクオート処理がされエラーになることがあったようです。
3.2.15ではこの問題が修正され、lock_versionをquoteする処理に型情報が付与されるようになったので、エラーは発生しなくなりました。

ActiveSupportの変更点

ActiveSupport::Cache::FileStore#cleanup

ActiveSupport::Cache::FileStore#cleanupが、each_keyメソッドに依存して正しく動作していませんでした。
3.2.15ではこれが修正され、cleanupを呼ぶと正しく期限切れキャッシュが削除されるようになりました。

TaggedLogging

TaggedLoggingがrespond_to_missing?を実装しました。これは@loggerのrespond_to?に委譲されます。
特別に機能が変わるわけではありませんが、method_missingを使う際のベストプラクティスが適用された形になります。

その他

ActionMailer, ActionPack, ActiveModel, ActiveResource, Railtiesには、機能変更はありません。

まとめ

以上、CHANGELOGとコミットログをまとめてみました。
地味に影響範囲が大きいバグが多数含まれているので、しっかりテストしつつ早めにアップデートしたいですね。

またRails 4のパッチリリースがなかなかでないのが地味に気になりますが、3.2系もいよいよ終盤なので、より積極的にRails 4を使っていきたいです。


Ruby on Rails 4.0.1リリース!大量のバグ修正、3系からの移行も少し簡単になりました

$
0
0

本日、Ruby on Rails 4.0の最初のアップデートである、Rails 4.0.1がリリースされました。

6月に4.0がリリースされてから約4ヶ月、かなり久しぶりのアップデートとなりますが、どのような変更が含まれているのでしょうか。

まとめ

量が多いので最初にまとめです。

今回のリリースは大量のdiffを含みますが、そのほとんどは細かいバグ修正です。
Railsのリリース時期的には3.2.13 → Rails 4.0.0 → 3.2.14 → 3.2.15 → Rails 4.0.1となるため、3.2.14以降で適用されていた細かい修正も取り込まれています。4系が3系にようやく追いつきました。

4系はなんかデグレっぽいバグや非互換が多いなーと思っていた箇所が、ほぼ解消されいる印象です。
現在、有名プロジェクトで4系への移行があまり進んでいないように感じますが、Rails 4.0.1のリリースが移行の引き金になるかもしれません。

重要な変更点

orderの仕様変更

ActiveRecordで、order()を複数回呼んだときの優先順位が逆になりました。先に呼んだ方が優先されるようになります。
もともとRails 4.0.0では3系の逆になっていたので、元に戻ったことになります。

User.order("name asc").order("created_at desc")

# Rails 4.0.0
# SELECT * FROM users ORDER BY created_at desc, name asc

# Rails 4.0.1
# SELECT * FROM users ORDER BY name asc, created_at desc

非常に影響範囲が大きいので、注意が必要です。gemの対応も確認する必要がありそうです。
逆に、3系からのアップデートをこれから行う人には朗報ですね。

その他、パフォーマンス改善が含まれています。

ActionMailer

defaultはProcの場合のみcallされるようになった

ActionMailerでは、以下のようにしてdefault設定ができますが、設定値にはProcを渡すこともできます。

class MyMailer < ActionMailer::Base
  default from: 'no-reply@example.com',
          subject: -> { "MAIL #{Time.now}" }
end

Rails 4.0.0では、設定値がProcでなくても、to_procを受け取る場合はto_procしてProcとして扱っていました。

しかしこの場合、たとえばArrayにto_procを定義した場合、ActionMailerのdefaultに配列を指定できなくなってしまいます。
Symbol#to_procが存在するのはもちろんのこと、ArrayやHashにto_procを定義するテクニックは便利なので、割と使われています。

default指定するときには上記サンプルコードのように素直にProcを渡せば良いため、Rails 4.0.1ではto_procによる変換は行われなくなりました。

ActionPack

redirect時にSCRIPT_NAMEが考慮されるようになった

Mountable Engine内でredirectを使い、そのEngineをアプリからサブディレクトリにマウントした場合、Rails 4.0.0ではエラーになっていました。
Rails 4.0.1ではこれが修正されています。

サンプルプログラムとしては以下のようなものです。

# mountable engine作成
rails plugin new myblog --mountable

# myblog/config/routes.rb
Myblog::Engine.routes.draw do
  get '/admin' => redirect('admin/dashboard')
end

# myblog/test/dummy/config/routes.rb
Rails.application.routes.draw do
  mount Myblog::Engine => "/myblog"
end

# Rails 4.0.0
Started GET “/myblog/admin” for 127.0.0.1 at 2013-10-26 14:26:07 +0900
ERROR URI::InvalidURIError: the scheme http does not accept registry part: localhost:3000admin (or bad hostname

# Rails 4.0.1
Started GET “/myblog/admin” for 127.0.0.1 at 2013-10-26 14:30:41 +0900
Started GET “/myblog/admin/dashboard” for 127.0.0.1 at 2013-10-26 14:30:41 +0900

意図しないIP spoofing attackエラーが修正された

Rails 3.2.15でも修正された意図しないIpSpoofAttackErrorが発生する問題が修正されました。

StrongParametersでネストした数値キーに関するバグが修正された

StrongParametersで以下のようになるバグが修正されました。

# これは当たり前
params = ActionController::Parameters.new(user: { data: { "name" => "hello" }})
params.require(:user).permit(data: ['name'])
=> {"data"=>{"name"=>"hello"}}

# キーが数値だと、Rails 4.0.0では値がnilになる
params = ActionController::Parameters.new(user: { data: { "1" => "hello" }})
params.require(:user).permit(data: ['1'])
=> {"data"=>{"name"=>nil}} # Rails 4.0.0
=> {"data"=>{"name"=>"hello"}} # Rails 4.0.1

HTTPアクセス時にSTSヘッダが送出されないようになった

HTTPSでないHTTPのときにSTS(Strict Transport Security)ヘッダを送出するのは無意味なので、HTTPSのときのみ送出するようになりました。
当然ですが、config.force_sslがtrueのときのみの影響です。

actionにif/falseとアクション指定が両方あるときの挙動

before_actionやafter_actionには、onlyやexceptでactionを限定することができるほか、if/unlessで条件指定することができます。
このとき、Rails 4.0.0では先にif/unless条件がチェックされていましたが、先にaction条件がチェックされるようになりました。

たとえば以下のように2回に1回フィルタを実行するようなものがあったとして、Rails 4.0.0ではshowアクションでもカウンタが増えていました。Rails 4.0.1ではindexアクションでのみカウントされます。

class UsersController < ApplicationController
  before_action :myfilter, only: [:index], if: -> { ($c = $c.to_i + 1).even? }
end

※Rails 3系では大分前に対応されていたので、Rails 4.0.0のデグレが修正された形です。

エスケープ文字が含まれるときのroutingやcurrent_page?判定が修正された

URLのパス文字列をパーセントエンコーディングする際、Railsは大文字を使いますが、nginxがリバースプロキシとして動作するときは小文字で送信してきます。
そのため、Rails 4.0.0ではcurrent_page?が意図せずfalseを返したり、routingにマッチしないということがありました。
Rails 4.0.1ではこの問題が修正され、パーセントエンコーディングはデコードされた状態で比較されます。

TestRequestでREMOTE_ADDRなどをオーバーライドできるようになった

ActionDispatch::TestRequest.newにHashを渡すことで、REMOTE_ADDR, HTTP_HOST, HTTP_USER_AGENTをオーバーライドできるようになりました。

text_areaにvalue=nilを指定すると空文字で上書きされるようになった

以下のようなコードの場合、Rails 4.0.0ではform_forに渡したオブジェクトのcommentがvalueとして出力されていました。
Rails 4.0.1では、valueがnilで上書きされるようになりました。これでtext_fieldと同じ挙動になりました。

f.text_area :comment, value: nil

link_toにブロックとURLハッシュを渡したときのバグが修正された

例を見れば一目瞭然ですが、変なバグが修正されました。

link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') }

# Rails 4.0.0
# => "<a action=\"bar\" controller=\"foo\"><span>Example site</span></a>"

# Rails 4.0.1
# => "<a href=\"/foo/bar\"><span>Example site</span></a>"

partialを再帰的に呼んだときのStack Level Too Deepが修正された

コメントが子コメントを持つ掲示板などで以下のようなコードを書いた場合、Rails 4.0.0ではStack Level Too Deepが発生していました。

# app/views/comments/_comment.html.erb
<%= render partial: 'comments/comment', collection: comment.children %>

Rails 4.0.1ではこの問題が修正されています。

date_fieldにvalueを渡せるようになった

以下のような形式で、date_field/datetime_field/color_fieldにvalueを設定できるようになりました。

<%= date_field 'user', 'birthday', value: Date.new(2000,10,1) %>

これは、Rails 4.0.0ではなぜか無視されていたものです。

link_to_unlessが常時エスケープされるようになった

こちらでも紹介しているRails 3.2.14の変更が適用されました。

ActiveModel

has_secure_passwordがBcryptのcostを参照するようになった

Rails 4.0.0ではhas_secure_passwordはBCrypt::Engine::DEFAULT_COSTを参照していましたが、Rails 4.0.1ではBCrypt::Engine.costを参照するようになりました。

ところで、bcrypt-rubyは仕様上costが31までしか設定できないですが、stretchingを1000回とかにしたい時は自前でやるしかないんですかね?31回ってえらく少ない気がするのですが、詳しい方いたら教えてください。

inclusion/exclusion validatorでRange指定に関するバグが修正された

inclusion/exclusionのvalidatorでは、Range指定した場合、Range#cover?が使われます。
この特性上、以下のような問題が発生していました。

class Student < ActiveRecord::Base
  # 成績はA,B,C,Dの4段階
  validates :grade, inclusion: { in: :A..:D }
end

Student.new(grade: :AB).valid?
# => true

これではよろしくないので、Range#cover?は数値Rangeの場合のみ使われるようになりました。

ActiveRecord

NullRelation#pluckが複数の引数を受け取るようになった

Rails 4ではpluckが複数カラムに対応しましたが、Rails 4.0.0ではNullRelationだけはなぜか複数カラムに非対応でした。
Rails 4.0.1では複数カラムに対応し、通常のRelationと互換性が保たれます。

# Rails 4.0.0
User.pluck(:id, :name) # => [[1, "Taro"], [2, "Jiro"]]
User.none.pluck(:id, :name) # => ArgumentError

# Rails 4.0.1
User.pluck(:id, :name) # => [[1, "Taro"], [2, "Jiro"]]
User.none.pluck(:id, :name) # => []

orderがコーテーションで囲まれるようになった

order(:id)のようにした場合、カラム名がコーテーションで囲まれるようになりました。

Post.order(:id).to_sql
# Rails 4.0.0
# => ... ORDER BY "posts".id ASC

# Rails 4.0.1
# => ... ORDER BY "posts"."id" ASC

11/3追記
はてブでご指摘頂きましたが、これが影響するのはorderの引数にシンボルを渡したときのみです。
Post.order(id: :desc)のようにすれば4.0.0でも4.0.1でもコーテーションで囲まれます。
Post.order('id')のようにすれば4.0.0でも4.0.1でも囲まれません。

whereでプレースホルダー形式を指定したときにサブクエリが生成されるようになった

以下のように、whereに配列で指定した場合にも、サブクエリが発行されるようになりました。

User.where(id: User.where(id: 1))
# =>  SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."id" = 1)

# Rails 4.0.0
User.where("id in (?)", User.where(id: 1).select(:id))
# => SELECT id FROM "users" WHERE "users"."id" = 1
# => SELECT "users".* FROM "users" WHERE (id in (1))

# Rails 4.0.1
User.where("id in (?)", User.where(id: 1).select(:id))
# => SELECT "users".* FROM "users" WHERE (id in (SELECT id FROM "users" WHERE "users"."id" = 1))

INを発行するには通常配列で指定するため、これはクエリ数が減って嬉しいですね。

joinしたときにincludesの指定が消えるバグが修正された

Rails 4.0.0には、Relationに対してincludesでjoinすると、selectで指定した値が消えるバグがありました。
to_sqlでは発生しなくて分かりづらいのですが、実行すると以下のようになります。

# Rails 4.0.0
User.select('name AS hoge').order('hoge').includes(:posts).where(posts: { id:1})
# SQLite3::SQLException: no such column: hoge:
# SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1,
# "posts"."id" AS t1_r0, "posts"."user_id" AS t1_r1 FROM "users"
# LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# WHERE "posts"."id" = 1  ORDER BY hoge

# Rails 4.0.1
User.select('name AS hoge').order('hoge').includes(:posts).where(posts: { id:1})
=> #<ActiveRecord::Relation []>
# SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1,
# "posts"."id" AS t1_r0, "posts"."user_id" AS t1_r1, name AS hoge FROM "users"
# LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# WHERE "posts"."id" = 1 ORDER BY hoge

NullRelationがwhere_values_hashを上書きしなくなった

Rails 4.0.0では、NullRelationがwhere_values_hashを上書きしていたため、Relationに設定されていたwhereがクリアされていました。
Rails 4.0.1ではこれが改善され、NullRelationでもwhere_values_hashが維持されるようになりました。

これは、次のような違いをもたらします。

# Rails 4.0.0
Post.where(user_id: 1).none.build
=> #<Post id: nil, body: nil, user_id: nil>

# Rails 4.0.1
Post.where(user_id: 1).none.build
=> #<Post id: nil, body: nil, user_id: 1>

Rails 4.0.1のほうが直感的な挙動ですね。

inverse_ofが設定されていてもfind([1])がArrayを返すようになった

Post.find([1,2])のようにすると、複数のデータを配列で取得できます。Post.find([1])だと当然、要素数1のArrayが取得できます。
しかしRails 4.0.0では、inverse_ofが設定されている場合に要素数が1だとArrayになりませんでした。Rails 4.0.1ではこれが修正されています。

class User < ActiveRecord::Base
  has_many :posts, inverse_of: :user
end

class Post < ActiveRecord::Base
  belongs_to :user, inverse_of: :posts
end

# Rails 4.0.0
User.first.posts.find([1]) # => #<Post id: 1>
User.first.posts.find([1,2]) # => [#<Post id: 1>, #<Post id: 2>]

# Rails 4.0.1
User.first.posts.find([1]) # => [#<Post id: 1>]
User.first.posts.find([1,2]) # => [#<Post id: 1>, #<Post id: 2>]

inverse_ofが設定されていているcallbackはメモリ上のparentを参照するようになった

こちらでも紹介しているRails 3.2.15の修正が適用され、inverse_ofが適用されているオブジェクトにcallbackを設定した場合、parentの参照はDBアクセスをしないようになりました。

特定条件でjoinsが不正なSQLを出力するバグが修正された

has_manyの第2引数にprocを渡して条件を追加し、その条件としてEQとそれ以外を組み合わせた場合、symbol指定でjoinしたときに不正なSQLが出力される問題が修正されました。

class User < ActiveRecord::Base
  has_many :hoge_posts,
           -> { where(title: 'HOGE').where('body LIKE "%HOGE%"') },
           class_name: 'Post'
end

class Post < ActiveRecord::Base
end

# Rails 4.0.0
User.joins(:hoge_posts)
# SQLite3::SQLException: syntax error
# SELECT "users".* FROM "users" INNER JOIN "posts"
# ON "posts"."user_id" = "users"."id"
# AND "posts"."title" = "HOGE", (body LIKE "%HOGE%")

# Rails 4.0.1
User.joins(:hoge_posts)
# SELECT "users".* FROM "users" INNER JOIN "posts"
# ON "posts"."user_id" = "users"."id"
# AND "posts"."title" = "HOGE" AND (body LIKE "%HOGE%")

find_in_batchesとfind_eachがlogger無しで動くようになった

find_in_batchesとfind_eachは、Rails 4.0.0ではActiveRecord::Base.loggerがnilの場合にエラーになっていましたが、Rails 4.0.1ではこれが修正されました。
(単純にログが出力されないようになりました)

has_oneでの空トランザクションが抑制された

以下のような無駄なトランザクションが発行されないようになりました。

# User has_one Accountとする

# Rails 4.0.0
User.new.account = Account.new
# BEGIN TRANSACTION
# COMMIT TRANSACTION

# Rails 4.0.1
User.new.account = Account.new
# (NO SQL)

inverse_ofなassociationでfindしたときに全件取得しなくなった

inverse_ofのassociationにfindを呼んだ場合、Rails 4.0.0では問答無用で全件取得してメモリに展開してから選択していました。
これはchildrenが多いときに壊滅的に遅くなるので、Rails 4.0.1では必要なもののみfindするようになりました。findしたものはinverse_ofなのでメモリ上に残ります。

class User < ActiveRecord::Base
  has_many :posts, inverse_of: :user
end

class Post < ActiveRecord::Base
  belongs_to :user, inverse_of: :posts
end

# Rails 4.0.0
user.posts.find(1)
# user.postsが全件取得されている!
# SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1

# Rails 4.0.1
user.posts.find(1)
# postsは1件しか取得しない
# SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 AND "posts"."id" = 1

exists?がtrue/falseを返すようになった

ActiveRecord::FinderMethods#exists?が必ずtrue/falseを返すようになりました。
Rails 4.0.0ではnilだったり1だったり色々あったのでわかりやすくなりました。

fixturesがsymlinkに対応

こちらでも紹介しているRails 3.2.15の修正が適用され、fixturesディレクトリのsymbolic linkが辿られるようになりました。

autosaveではinsertの前にdeleteが実行されるようになった

autosaveなassociationで、mark_for_destructionしている場合、INSERT文が発行される前にDELETE文が発行されるようになりました。
これにより、UNIQUE制約が付いているDBでのエラーが解消されます。

class User < ActiveRecord::Base
  has_many :posts

  # accepts_nested_attributes_forをするとautosave: trueになる
  accepts_nested_attributes_for :posts
end

Post.connection.add_index :body, unique: true

@user.posts.create(body: 'TEST')
@user.posts[0].mark_for_destruction
@user.posts.build(body: 'TEST')
@user.save!
# => Rails 4.0.0ではActiveRecord::RecordNotUniqueが発生

counter_cacheが増えないバグが修正された

4.0.0でデグレしていた箇所が修正されました。

class User < ActiveRecord::Base
  has_many: :posts
end

class Post < ActiveRecord::Base
  has_many: :user, counter_cache: 'posts_count'
end

@user.posts << Post.create(body: 'HELLO')
# Rails 4.0.0だとposts_countがインクリメントされない!

ActiveSupport

ActiveSupport::Cache::FileStore#cleanup

こちらでも紹介しているRails 3.2.15の修正が適用され、ActiveSupport::Cache::FileStore#cleanupの問題が修正されました。

ActiveSupport::Deprecation.behaviorに:raiseが追加された

以下のようにすることで、DEPRECATEDな機能を呼び出したときにActiveSupport::DeprecationExceptionをraiseさせることができるようになりました。

# config/environments/development.rb
ActiveSupport::Deprecation.behavior = :raise

テストを回すときには便利そうです。

DateTimeにusecとnsecが追加された

DateTimeにusecとnsecが追加されました。ActiveSupport::TimeWithZoneもあわせて対応します。

Time.zone = 'Tokyo'
DateTime.now.in_time_zone.usec
# => 0 (Rails 4.0.0)
# => 94805 (Rails 4.0.1)

Time.atがUTC offsetを維持するようになった

例を見るとわかりやすいと思います。

Time.zone = 'Tokyo'

# Rails 4.0.0
Time.now # => 2013-10-26 20:49:17 +0900
Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000)) # => 2000-01-01 09:00:00 +0900
Time.at(Time.utc(2000)).zone # => "JST"

# Rails 4.0.1
Time.now # => 2013-10-26 20:49:17 +0900
Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000)).zone # => "UTC"

Railties

イベント名が変更された

loggerでsubscribeするときなどに使うイベント名が、action_dispatch.requestからrequest.action_dispatchに変更されました。

config.log_levelがカスタムロガーにも対応した

config.log_level = :infoのように設定したログレベルが、標準のLoggerだけでなくカスタムで設定したLoggerにも適用されるようになりました。

skip-javascript時にgeneratorがturbolinksのパラメータを出力しなくなった

Rails 4.0.0では、rails newしたときに、app/views/layout/application.html.erbで以下のようにturbolink設定が含まれます。

<%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>

ただし、rails new –skip-javascriptしたときにはこれは無駄なので、Rails 4.0.1ではskip-javascript時にはturbolinksのattributeが付かないようになりました。

最後に

それなりに変更点を書き出してみましたが、他にもたくさんあります。
より詳しく知りたい方はソースコードを読んでみてください。

active_support/core_extの変化を見る

$
0
0

Railsの便利さを支えるActiveSupport。特にcore_extは単体で使うことも多いと思います。

基本的な機能ながら、バージョンごとに地味に機能修正が行われているので、わかりやすいものを2個取り上げてみました。

3.2で挙動が変わったblank?

blank?はご存じ、「空ならtrueを返す」便利メソッドです。

blank?
オブジェクトが空ならtrueを、そうでなければfalseを返す
present?
!blank?を返す
presence
blank?ならnilを、そうでなければselfを返す

blank?は基本的に以下のような仕組みです。

  • empty?が定義されているとき: empty?を呼び出す
  • empty?が定義されていないとき: !selfを返す

たとえば[]にはempty?があるので、空配列はblank?がtrueになります。
nilにはempty?がないので、!selfを評価した結果blank?はtrueになります。
Object.newにはempty?がないので、!selfを評価した結果blank?はfalseになります。

実際にはArrayやNilClassなどには高速化のため別定義がしてあります。

ここで、Stringだけは違うルールでblank?が個別定義されているので注意が必要です。

Stringにはempty?が存在しますが、これは空文字であるかどうかを調べるメソッドです。
String#blank?はこれを使わず、「空白文字は空と見なす」として動作します。
この「空白文字」の定義がバージョンによって異なります。

# ActiveSupport 3.1.0
def blank?
  self !~ /\S/
end
# ActiveSupport 3.2.0
NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
def blank?
  # 1.8 does not takes [:space:] properly
  if encoding_aware?
    self !~ /[^[:space:]]/
  else
    self !~ NON_WHITESPACE_REGEXP
  end 
end
# ActiveSupport 4.0.0
def blank?
  self !~ /[^[:space:]]/
end

3.1までは/\S/が含まれていれば「空ではない」と見なされました。
つまり、「半角スペース」「タブ」「CR」「LF」の4文字以外は空白でないとされ、全角スペースは有効な文字とされていました。

3.2以降では[:space:]というPOSIX文字クラスに変更されています。
この文字クラスには全角スペースなども含まれるため、全角スペースのみの文字列にblank?を実行すると、

  • 3.1まで: false
  • 3.2から: true

という結果になります。

ちなみに[:space:]文字クラスの詳細は、RubyDocによると鬼車を参照しろと書いてあります。
http://www.geocities.jp/kosako3/oniguruma/doc/RE.txtによると、厳密には以下の文字になっています(Unicodeの場合)。

Space_Separator | Line_Separator | Paragraph_Separator |
0009 | 000A | 000B | 000C | 000D | 0085

Space_SeparatorはUnicodeのZsカテゴリ、Line_SeparatorはZlカテゴリU+2028だけ)、Paragraph_SeparatorはZpカテゴリ(U+2029だけ)です。

Unicode規定されている空白文字はすべてblank?になるようになっています。

4.0で挙動が変わったtry

基本メソッドtryも、4.0で挙動が変わりました。

# ActiveSupport 3.2.0
def try(*a, &b)
  if a.empty? && block_given?
    yield self
  else
    __send__(*a, &b)
  end
end
# ActiveSupport 4.0.0
def try(*a, &b) 
  if a.empty? && block_given?
    yield self
  else
    public_send(*a, &b) if respond_to?(a.first)
  end 
end 

def try!(*a, &b) 
  if a.empty? && block_given?
    yield self
  else
    public_send(*a, &b) 
  end 
end

主な違いは2点です。

sendがpublic_sendになった
直接呼び出すのと対称性が高まりました。privateメソッドを呼び出せることを期待していたコードは動かなくなります。
respond_to?チェックをするようになった
従来のtryはtry!になり、tryではrespond_to?チェックが事前に実行されます。4.0のtryでは、存在しないメソッドを指定してもNoMethodErrorは発生せず、何もしません。

まとめ

Ruby界隈は良くも悪くも進歩と仕様変更が早いので、常に最新ウォッチが大事ですね。
更新情報はCHANGELOGをブックマークに入れておくのがたぶん手っ取り早いと思います。幸いテストがしっかりしているので、挙動が分からなければそちらを見ればOKです。

あと、Rails以外でRubyを使うときも、必ずgemはbundleで管理し、Gemfile.lockをリポジトリに入れておく方が良いと思います。

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

$
0
0

こんにちは、hachi8833です。今回は、自分が知りたかった、ActiveRecordモデルのリファクタリングに関する記事を翻訳いたしました。1年前の記事なのでRails 3が前提ですが、Rails 4でも基本的には変わらないと思います。リンクは可能なものについては日本語のものに置き換えています。

なお、ここでご紹介したオブジェクトは、app以下にそれぞれ以下のようにフォルダを追加してそこに配置します。
refactor

肥大化したActiveRecordモデルをリファクタリングする7つの方法

(元記事: 7 Patterns to Refactor Fat ActiveRecord Models)
Posted by @brynary on Oct 17th, 2012 (Code Climate Blog)

Railsアプリケーションの品質を高めるためにチーム内でCode Climateを使用していれば、モデルの肥大化を自然と避けるようになるでしょう。モデルが肥大化(ファットモデル)すると、大規模アプリケーションのメンテナンスが困難になります。ファットモデルは、コントローラがドメインロジックで散らかってしまうよりは1段階だけましであるとはいえ、たいていの場合Single Responsibility Principle (SRP:単一責任の原則)の適用に失敗した状態であると言えます。

SRPの適用は、元々難しいものではありません。ActiveRecordクラスは永続性と関連付けを扱うものであり、それ以外のものではありません。しかしクラスはじわじわ成長していきます。永続性について本質的に責任を持つオブジェクトは、やがて事実上ビジネスロジックも持つようになるのです。1年2年が経過すると、User クラスには500行ものコードがはびこり、パブリックなインターフェイスには数百ものメソッドが追加されることでしょう。それに続くのはコールバック地獄です。

アプリケーションに何か本質的に複雑な要素を追加したら、ちょうどケーキのタネをケーキ型に流し込むのと同じように、それらを小規模かつカプセル化されたオブジェクト群(あるいはより上位のモジュール)に整然と配置することが目標になります。ファットモデルは、さしずめタネをケーキ型に流し込むときに見つかるダマ(混ざらなかった粉の固まり)のようなものでしょう。これらのダマを砕いて、ロジックが等分に広がって配置されるようにしなければなりません。これを繰り返し、それらが最終的に、シンプルで、きちんと定義されたインターフェイスを持つ一連のオブジェクトとなって、それらが見事に協調動作するようにしましょう。

そうは言っても、きっとこう思う人もいることでしょう。

“でもRailsでちゃんとOOPするのってめちゃくちゃ大変ぢゃなくね?!”

私も以前は同じことを思ってました。でも若干の調査と実践の結果、RailsというフレームワークはOOPを妨げてなどいないという結論に達しました。スケールできないでいるのはRailsのフレームワークではなく、従来のRailsの慣習・流儀の方です。より具体的に言えば、Active Recordパターンできちんと扱える範囲を超えるような複雑な要素を扱うための定番の手法がまだないのです。幸いにも、オブジェクト指向における一般的な原則とベストプラクティスというものがあるので、Railsに欠けている部分にこれらを適用することができます。

[その前に]ファットモデルからミックスインで展開しないこと

肥大化したActiveRecordクラスから単に一連のメソッドを切り出して “concerns” やモジュールに移動するのはよくありません。移動したところで、後でまた1つのモデルの中でミックスインされてしまうのですから。いつだったか、こんなことを言っていた人がいました。

“app/concerns ディレクトリを使っているようなアプリケーションって、だいたい後から頭痛の種になる(=concerning)んだよね”

私もそう思います。ミックスインよりも、継承で構成する方がよいと思います継承よりコンポジションの方がよいと思います。このようなミックスインは、部屋に散らかっているガラクタを引き出しに押し込めてピシャリと閉めたのと変わりません。一見片付いているように見えても、引き出しの中はぐちゃぐちゃ、どこに何があるのかを調べるだけでも大変です。ドメインモデルを明らかにするために必要な分解と再構成を実装するのも並大抵ではありません。
これはもうリファクタリングするしかないでしょう。

1. Valueオブジェクト

Valueオブジェクトは、異なるオブジェクト同士であっても値が等しければ等しいと見なされる、シンプルなオブジェクトです。Valueオブジェクトは変更不可能であるのが普通です。Rubyの標準ライブラリにはDateURIPathname などのValueオブジェクトがありますが、Railsアプリケーションでもドメイン固有のValueオブジェクトを定義できますし、そうすべきです。ActiveRecordからValueオブジェクトへの展開は、すぐにもメリットの得られるリファクタリングです。

Railsでは、ロジックが関連付けられている属性が1つ以上ある場合にはValueオブジェクトが有用です。単なるテキストフィールドやカウンタ以上の要素は、何でもValueオブジェクトの候補になりえます。

ちょうど著者が仕事をしている某テキストメッセージングアプリケーションには、PhoneNumber というValueオブジェクトがあります。そして某eコマースアプリケーションではMoneyクラスを必要としています。私たちのCode Climateには RatingというValueオブジェクトがあり、受け取ったクラスやモジュールのランキングをAからFまでの段階で表します。ここではRuby のStringクラスのインスタンスを使用することもできます(実際使用していました)が、このRatingを使用すると以下のように振る舞いとデータを一体化することができます。

class Rating
  include Comparable

  def self.from_cost(cost)
    if cost <= 2
      new("A")
    elsif cost <= 4
      new("B")
    elsif cost <= 8
      new("C")
    elsif cost <= 16
      new("D")
    else
      new("F")
    end
  end

  def initialize(letter)
    @letter = letter
  end

  def better_than?(other)
    self > other
  end

  def <=>(other)
    other.to_s <=> to_s
  end

  def hash
    @letter.hash
  end

  def eql?(other)
    to_s == other.to_s
  end

  def to_s
    @letter.to_s
  end
end

次にすべてのConstantSnapshotRatingのインスタンスをパブリックなインターフェイスに公開します。

class ConstantSnapshot < ActiveRecord::Base
  # …

  def rating
    @rating ||= Rating.from_cost(cost)
  end
end

これによりConstantSnapshotがスリムになるだけでなく、他にも多くの利点があります。

  • #worse_than?メソッドと#better_than?メソッドは、レートを比較する場合には<>などのRubyの組み込み演算子よりも適切です。
  • #hash#eql?を定義しておけばRatingをハッシュキーとして使用できます。Code Climateではこれを使用して、定数をレートごとにEnumberable#group_byでグループ化しています。
  • #to_sメソッドを定義してあるので、Ratingを簡単に文字列やテンプレートに変換できます。
  • このクラス定義は、ファクトリーメソッドを導入する場合にも便利です。矯正コスト (=クラスの「臭い」を除去するのにかかる時間) に見合う、正しい Rating を得られます。

2. Serviceオブジェクト

アクションによってはServiceオブジェクトを使用して操作をカプセル化することができるものがあります。著者の場合、以下の基準に1つでも合えばServiceオブジェクトの導入を検討します。

  • アクションが複雑になる場合 (決算期の終わりに帳簿をクローズする、など)
  • アクションが複数のモデルにわたって動作する場合 (eコマースの購入でOrder, CreditCard, Customer を使用する、など)
  • アクションから外部サービスとやりとりする場合 (SNSに投稿する、など)
  • アクションが背後のモデルの中核をなすものではない場合 (一定期間ごとに古くなったデータを消去する、など)
  • アクションの実行方法が多岐にわたる場合 (認証をアクセストークンやパスワードで行なう、など)。これはGoF (Gang of Four) のStrategyパターンです。

例として、User#authenticate メソッドを取り出して UserAuthenticatorに配置しましょう。

class UserAuthenticator
  def initialize(user)
    @user = user
  end

  def authenticate(unencrypted_password)
    return false unless @user

    if BCrypt::Password.new(@user.password_digest) == unencrypted_password
      @user
    else
      false
    end
  end
end

このとき、SessionsController は以下のような感じになります。

class SessionsController < ApplicationController
  def create
    user = User.where(email: params[:email]).first

    if UserAuthenticator.new(user).authenticate(params[:password])
      self.current_user = user
      redirect_to dashboard_path
    else
      flash[:alert] = "Login failed."
      render "new"
    end
  end
end

3. Formオブジェクト

1つのフォーム送信で複数のActiveRecordモデルを更新する場合、Formオブジェクトを使用して集約することができます。Formオブジェクトを使用すれば、(個人的には使用を避けたい) accepts_nested_attributes_forよりもずっときれいなコードになります。CompanyUserを同時に作成するユーザー登録フォームを例にとってみましょう。

class Signup
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attr_reader :user
  attr_reader :company

  attribute :name, String
  attribute :company_name, String
  attribute :email, String

  validates :email, presence: true
  # … more validations …

  # Forms are never themselves persisted
  def persisted?
    false
  end

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

private

  def persist!
    @company = Company.create!(name: company_name)
    @user = @company.users.create!(name: name, email: email)
  end
end

これらのオブジェクトではVirtus gemを使用してActiveRecord的な属性機能を利用しています。FormオブジェクトはActiveRecordと同様に振る舞うので、コントローラは通常と変わらないものになります。

class SignupsController < ApplicationController
  def create
    @signup = Signup.new(params[:signup])

    if @signup.save
      redirect_to dashboard_path
    else
      render "new"
    end
  end
end

Formオブジェクトは、上のようなシンプルな例ではうまくいきますが、永続性のロジックが含まれていてフォームが複雑になるのであれば、Serviceオブジェクトも併用するのがよいでしょう。Validationロジックはコンテキストに依存することが多いのですが、Formオブジェクトを導入するメリットとして、ActiveRecord自身の中でvalidationを行なうという融通の効かない方法に代えて、validationロジックをコンテキストに応じた場所で定義できるというのがあります。

4. Queryオブジェクト

スコープやクラスメソッドなどのActiveRecordサブクラスが乱雑に定義された、複雑なSQLクエリがある場合は、Queryオブジェクトに展開することを検討します。1つのQueryオブジェクトは、ビジネスロジックに基づいた結果セットを1つだけ返す責任を持ちます。たとえば、訪問されないままになっているお試しを検索するQueryオブジェクトは以下のような感じになります。

class AbandonedTrialQuery
  def initialize(relation = Account.scoped)
    @relation = relation
  end

  def find_each(&block)
    @relation.
      where(plan: nil, invites_count: 0).
      find_each(&block)
  end
end

このオブジェクトを使ってバックグラウンドで以下のようにメールを送信します。

AbandonedTrialQuery.new.find_each do |account|
  account.send_offer_for_support
end

ActiveRecord::RelationインスタンスはRails 3によって第一級オブジェクトとして扱われるため、Queryオブジェクトでも多くの機能を使用できます。このおかげで、コンポジションを使用してクエリを結合できます。

old_accounts = Account.where("created_at < ?", 1.month.ago)
old_abandoned_trials = AbandonedTrialQuery.new(old_accounts)

この種のクラスのテストは個別には行ないません。オブジェクトのテストとデータベースのテストは同時に行うようにし、それによって正しい行が正しい順序で返されることと、結合(join)とeager loadingがすべて動作する(N + 1クエリ問題などを回避できている)ことを確認します。

5. Viewオブジェクト

表示にしか使わないようなロジックが必要な場合、それはモデルに置くべきではありません。「仮にアプリケーションのUIががらりと変わったら(たとえば音声駆動UIになったとしたら)、その時にもこれをモデルに置く必要があるだろうか」と自問自答してみましょう。モデルに置く必要のない表示ロジックであることがわかったら、ヘルパーに置くか、できればなるべくViewオブジェクト(訳注: これはRailsのビューテンプレートとは別です)に置くようにしましょう。

たとえば、Code ClimateではRails on Code Climateなどでコードベースのスナップショットに基づいたクラスのレート付けを行う円グラフを使用していますが、これらは次のようにViewオブジェクトにカプセル化できます。

class DonutChart
  def initialize(snapshot)
    @snapshot = snapshot
  end

  def cache_key
    @snapshot.id.to_s
  end

  def data
    # @snapshotからデータを取り出してJSON構造に変換するコードを置く
  end
end

ところで、著者はビューとERBテンプレート (Haml/Slimでも同じですが) が一対一で対応していることが多いことに気が付きました。そこで、Railsで使えるTwo Step Viewパターンを実装できないか調べ始めたのですが、今のところこれについては明快なソリューションを見つけられずにいます。

メモ: Railsコミュニティでよく使用されている「Presenter」という用語についてですが、この用語が他の用法と重複したり誤解を招いたりする可能性があるため、著者はこの用語を避けるようにしています。Presenterという語は、本記事で言うところのFormオブジェクトを説明するためにJay Fieldsによって導入されました。また、運の悪いことにRailsでは「View」という用語もいわゆる「(ビューの)テンプレート」を指すものとして使用しています。曖昧さを避けるため、著者はViewオブジェクトを「Viewモデル」と書くことがあります。

6. Policyオブジェクト

複雑な読み出し操作はそのオブジェクト自身で行なうのがふさわしいことがあります。著者はこのような場合にPolicyオブジェクトを検討します。Policyオブジェクトを使用することで、本質的でないロジック (分析用にどのユーザーをアクティブとみなすか、など) を、中核となるドメインオブジェクトから切り離すことができます。以下は例です。

class ActiveUserPolicy
  def initialize(user)
    @user = user
  end

  def active?
    @user.email_confirmed? &&
    @user.last_login_at > 14.days.ago
  end
end

このPolicyオブジェクトには1つのビジネスルールがカプセル化されています。このビジネスルールでは、emailが確認済みで、かつ2週間以内にログインしたことがあるユーザーをアクティブなユーザーとみなすようになっています。Policyオブジェクトは、複数のビジネスルール (特定のデータへのアクセスを許可するAuthorizer など) をカプセル化することもできます。

PolicyオブジェクトはServiceオブジェクトと似ていますが、著者はServiceオブジェクトは書き込み操作、Policyオブジェクトは読み出し操作と使い分けています。これらはQueryオブジェクトとも似ていますが、Policyオブジェクトはメモリに読み込み済みのドメインモデルについて操作を行なうのに対し、Queryオブジェクトは特定の結果セットを返す「SQLの実行」に特化している点が異なります。

7. Decorator

Decoratorは既存の操作に関する機能を階層化することによって、コールバックとよく似た機能を果たします。Decoratorは、特定の環境でしか実行したくないコールバックロジックがある場合や、ある機能をモデルに含めるとモデルの担当責任が増え過ぎる(=モデルが肥大化する)場合に便利です。

あるブログ投稿にコメントが付くと誰かのFacebookウォールに自動的に投稿されるようにしたとします。この場合、このロジック自体をCommentクラスにハードコードしなければならないということにはなりません。コールバックに多くの責任を負わせすぎると、テストの実行が遅くなり、不安定になるという形で兆候が現れます。こうした副作用は、何の関連もないテストケースから取り除くべきでしょう。

Facebookへの投稿ロジックをDecoracorに展開する方法を以下に示します。

class FacebookCommentNotifier
  def initialize(comment)
    @comment = comment
  end

  def save
    @comment.save && post_to_wall
  end

private

  def post_to_wall
    Facebook.post(title: @comment.title, user: @comment.author)
  end
end

このDecoratorをコントローラで以下のように使用します。

class CommentsController < ApplicationController
  def create
    @comment = FacebookCommentNotifier.new(Comment.new(params[:comment]))

    if @comment.save
      redirect_to blog_path, notice: "Your comment was posted."
    else
      render "new"
    end
  end
end

DecoratorはServiceオブジェクトとは異なります。Serviceオブジェクトは既存のインターフェイスに対する責任を階層化しますが、これをDecorator化すると、FacebookCommentNotifier インスタンスをあたかも単なるCommentであるかのように取り扱います。Rubyは、メタプログラミングを使用してDecoratorを簡単に作成するための仕組みを標準ライブラリに多数備えています。

最後に

複雑なモデル層をうまく取り扱うためのツールは、Railsアプリケーションにも多数存在します。これらのツールを使用するためにRailsを捨てる必要などありません。ActiveRecordsは素晴らしいライブラリですが、これだけに頼っていてはどんなパターンも失敗します。ActiveRecordsは、極力永続的な振る舞いにとどめておくようにしてください。本記事で紹介したテクニックを一部だけでも適用して、自分のアプリケーションのドメインモデルに固まっているロジックを分散させることができれば、アプリケーションのメンテナンスはずっと容易になるでしょう。

本記事で紹介したパターンの多くはシンプルです。これらのオブジェクトは、いずれも「昔ながらのシンプルなRubyオブジェクト(Plain Old Ruby Objects: PORO)」であって、ただその使い方が異なるだけです。これはOOPの一部であり、OOPの美しさをなすものです。問題を解決するのにフレームワークやライブラリだけに頼る必要はありません。手法に適切な名前を付けることも重要な技法です。

本記事で紹介した7つの技法はいかがでしたでしょうか。お気に入りの技法は見つかりましたでしょうか。それはどんな理由でしょうか。コメントをお待ちしています。

追伸: この記事を気に入っていただけましたら、元記事の下にあるフォームからCode Climateのニュースレターをぜひ購読してみてください。OOPやRailsアプリケーションのリファクタリングなど、今回の記事のようなトピックを扱っています。記事のボリュームは控えめにしています。

より詳細な情報

本記事をレビューしてくれた皆様に感謝します: Steven Bristol, Piotr Solnica, Don Morrison, Jason Roelofs, Giles Bowkett, Justin Ko, Ernie Miller, Steve Klabnik, Pat Maddox, Sergey Nartimov, Nick Gauthier

Rails 4.0.2, 3.2.16リリース!重大なセキュリティFIXがあります

$
0
0

昨日、Ruby on Railsの重要なセキュリティアップデートである、Rails 4.0.2と3.2.16がリリースされました

このリリースには、5件(3.2.16には4件)のセキュリティFIXが含まれています。
重要度の高いものがあるため、早急なアップデートをしましょう。

CVE-2013-6416

simple_formatヘルパーのXSS脆弱性に関する修正です。
※4.0.2のみ。3.2系では元から発生しないため、3.2.16には含まれません。

simple_formatはhtml_optionsとしてHashを渡せますが、デフォルトで、このclass指定がHTMLエスケープされていませんでした。
class指定をユーザ入力による場合、容易にXSSが成立してしまいます。

simple_format "hello\nworld", class: '"><script>alert(1)</script>'
simple_formatの結果

simple_formatの結果

なお、該当の修正はこのコミットなのですが、小さいコミットで一瞬あれ?と思うけど面白いです。
SanitizeとEscapeは全然違うものなのに、うっかり混ざってしまっていたようですね。

何らかの理由でアップデートできない場合、simple_formatにclassを指定している箇所をすべてチェックし、手動でエスケープするようにしましょう。

https://groups.google.com/forum/#!topic/ruby-security-ann/5ZI1-H5OoIM

CVE-2013-6414

ActionViewのDoS脆弱性に関する修正です。

未修正の状態では、HTTPヘッダのformat部分を細工することで、ActionViewのキャッシュキーを操作することができます。
これにより、リクエストを繰り返すとキャッシュキーを無限に増大させることができ、最終的にメモリ不足によるサービス停止を引き起こすことが可能になります。

(具体的な攻撃方法はちょっと見ただけではわかりませんでした)

対策として、あらかじめ登録されている正当なMIMEタイプ以外は弾くように修正されました。

影響が大きいため、サポートが終了したRails 3.0と3.1系にもパッチが提供されています。
3.0/3.1を使っている場合や、何らかの理由でアップデートできない場合は、以下のパッチの適用を検討しましょう。
https://groups.google.com/forum/#!topic/ruby-security-ann/A-ebV4WxzKg

CVE-2013-6415

number_to_currencyのXSS脆弱性に関する修正です。

未修正の状態では、number_to_currencyに渡すunitはHTMLエスケープされません。
単位を「円」「ペリカ」「ゼニー」などユーザが変更できるようにしている場合、XSSが成立します。

対策として、デフォルトでunitはHTMLエスケープされるようになりました。html_safeした場合は従来通りの挙動になります。

number_to_currency 1000, unit: '<script>alert(1)</script>'
number_to_currencyの結果

number_to_currencyの結果

何らかの理由でアップデートできない場合、unitを手動でHTMLエスケープするようにしましょう。

number_to_currency 1000, unit: h(params[:unit])

https://groups.google.com/forum/#!topic/ruby-security-ann/9WiRn2nhfq0

CVE-2013-4491

反射型XSS脆弱性に関する修正です。

i18n gemが、以下のバージョン未満のときのみ発生します。つい最近作ったプロジェクトなら、問題ない可能性が高いです。

  • i18n-0.6.6 for Rails 4.0.x and 3.2.x applications
  • i18n-0.5.1 for Rails 3.1.x and 3.0.x applications

未修正の状態では、i18nで対応するキーが無い場合、デフォルトで以下のように見やすいHTML形式で結果が返ってきました。

t('hoge')
# => <span class="translation_missing" title="translation missing: hoge">hoge</span>

これは見やすくて便利なのですが、しかしi18nが上記対策バージョン未満の場合、このkeyがHTMLエスケープされません。

translationのキーをユーザが自由に入力できるパターンは少ないと思いますが、以下のような設定は多くのサイトで使われているのではないでしょうか。

I18n.locale = params[:locale]

そのため、たとえば以下のURLで、missing translationがある場合、ページの文字が大きくなったりします。

example.com/?locale="><h1>

対策として、デフォルトではHTML形式のmissing tranlation表示ではなく、プレインテキスト形式に変更になりました
仕様変更になりますので、ご注意ください。
HTML形式が必要な場合、rescue_format: :htmlを手動で指定しましょう。

何らかの理由でアップデートできない場合、以下の本文に記載のあるモンキーパッチをconfig/initializersに設置しましょう。
https://groups.google.com/forum/#!topic/ruby-security-ann/pLrh6DUw998

CVE-2013-6417

以前Techrachoでも紹介した、Rails 3.2.11で修正されたCVE-2013-0155ですが、対応が不完全だったため再修正されたものです。

Rails 3.2.11のときに、Rails::Requestは修正されていましたが、Rack::Requestのほうは未対策でした。
これにより、Rack Middlewareがパラメータを参照した場合に、従来と同じ問題が発生する可能性があります。一部のRack Middlewareでは、Railsに渡す前のパラメータを取得・加工していることがあるためです。

対策として、Rack::Requestでもdeep_munge(空ハッシュなどをnilに変換)されるようになりました。

何らかの理由でアップデートできない場合、Rack Middlewareがパラメータを参照している箇所を、慎重に検査するしかありません。
意外と多くのGemがRack Middlewareを追加しているので、注意が必要です。

https://groups.google.com/forum/#!topic/ruby-security-ann/niK4drpSHT4

まとめ

今回のセキュリティFIXは、潜在的というレベルではなく、多くのアプリで即座に影響のあるものが含まれています。
アップデートが容易になるよう、セキュリティFIXのみのリリースになっているほか、個別パッチもリリースされていますので、早急なアップデートをしたいところです。

【Rails】Prawnで縦書きを実現する

$
0
0

PrawnはRubyで使えるPDF生成ライブラリです。
Railsを使った案件でPDFを作成する場合は、よくPrawnを利用しています。
今回、縦書きに対応する必要があったのですが、Prawnが提供しているメソッドだけだと対応できないので、自作してみました。
基本的な考え方としては、「文字を1文字ずつ区切り縦に並べる」です。
後は自動改行や複数行になった際の中心の調整を対応していきます。

※今回の縦書きメソッドは宛名書き用に作成しています。
文章への対応となると約物等への対応が必要になり、ものすごく大変になります。
今回は長音符のみ90度回転させる方法で対応しています。
UTR50を見るとさらにy軸を中心に180度回転させるのが正しいように見えますが、今回は対応していません。
フォントに縦書き用のグリフがある場合は、それを使って対応することもできます。

今回のバージョン
Rails 3.2.14
prawn 0.12.0

Gemのインストール

gem install prawn
//Gemfileに記述する場合
gem prawn

縦書きメソッドの作成

# -*- coding: utf-8 -*-
require ("#{Rails.root}/lib/module/string_extention.rb")
# PDFドキュメント生成クラスであるPrawn::Documentを継承したクラスを作る
class ExtentionPrawnDocument < Prawn::Document
  YOHAKU = 2 #文字間隔調整の値
  # Options
  # at (required),  [x,y] The position at which to start the text
  # size
  # style
  # kerning boolean
  # center boolean automatically text start index with at
  # height New line when you exceed this value
  def vertical_text(text, options = {}) 
    letters = text.split(//) # 1文字分割して配列にする
    f_size = options[:size] || font_size # font_sizeはPrawn::Document内で定義されているメソッドです。
    options[:at] ||= [0, 0]
    origin_at = options[:at]

    # 自動改行
# height指定されていなければ改行しない。この高さを超えたら改行
    if options[:height] 
      height = options[:height].to_i
      letter_line_height = 0
      letters.each_with_index do |letter, i|
        letter_line_height = 0 and next if letter.breakcode?
        letter_line_height += f_size + YOHAKU
        if letter_line_height > height
          letters.insert(i, "\n")
          letter_line_height = 0
        end
      end
    end

    #改行して複数行になったときに自動で中央を調整するか
    if options[:center]
      line_count = calculate_line_count(letters)
      align_index = (f_size / 2) * (line_count -1)
      options[:at] = [options[:at][0] + align_index, options[:at][1]]
    end
    letters.each do |letter|
 # 改行コードの場合はx座標をずらして次へ
      if letter.breakcode?
        options[:at] = [options[:at][0] - f_size - YOHAKU, origin_at[1]]
        next
      end
# 長音符の場合は例外処理を行う-90度回転させる。ただ回転させるだけだとテキストのベースラインの関係から文字が重なるのでy座標を調整する
      if letter.macron? 
        options[:rotate] = -90
        options[:at] = [options[:at][0], options[:at][1] + f_size]
      else
        options[:rotate] = 0
      end
      draw_text(letter, options) # 文字の描画
      options[:at] = [options[:at][0], options[:at][1] - f_size] if letter.macron? #長音符の場合は例外処理したy座標を元に戻す
      options[:at] = [options[:at][0], options[:at][1] - f_size - YOHAKU] #y座標をフォントサイズ+余白分ずらす
    end
  end

  private
  # 行数を数えるメソッド
  # Args
  # arry: 1文字ごとの配列
  def calculate_line_count(arry)
    line_count = arry.count("\n")
    line_count = arry.count("\r") if line_count == 0
    line_count = arry.count("\r\n") if line_count == 0
    line_count += 1
    return line_count
  end

end

vertical_textのOptionはcenter, height以外はdraw_textで指定されているoptionになります。
draw_textにはその他にrotateがありますが、長音符対応で使用する関係上、
引数に指定されても意味のないものになるので、記載していません。

breakcode?とmacron?はStringクラスを拡張しています。

class String
 # 改行コードか
  def breakcode? 
    self == "\n" || self == "\r" || self == "\r\n"
  end
  # 長音符(ー)か
  def macron?
    self == "ー"
  end
end

縦書きメソッドの呼び出し

pdf = ExtentionPrawnDocument.new({:page_layout => :landscape, :page_size => 'A4', :left_margin => 0, :right_margin => 0, :top_margin => 0, :bottom_margin => 0})
pdf.vertical_text '縦書きサンプルでーす', at: [100, 100]
pdf.render

出力サンプル

フォントは青柳衡山フォントです。
tech

Haml で閉じタグに悪戦苦闘してRails を学んだお話

$
0
0

Rails を使い始めてからたまにテンプレートでHaml を使う機会があるのですが、先日下記HTML のようなものを書きたいと思いました。

<div class="hoge">
  <div class="fuga">item1</div>
  <div class="fuga">item2</div>
</div>
<div class="hoge">
  <div class="fuga">item3</div>
  <div class="fuga">item4</div>
</div>

まずはERB に起こしてみました。

<% items.each_with_index do |item, index| %>
  <% if index % 2 == 0 %>
    <div class="hoge">
  <% end %>
  <div class="fuga"><%= item.to_s %></div>
  <% if index % 2 == 1 %>
    </div>
  <% end %>
<% end %>
<%# 凄い雑です、奇数で終わる場合とか無視です %>

しかしこれをHaml で書こうとすると途端に面倒なことになります。何しろインデントが命のHaml です。

- items.each_with_index do |item, index|
  - if index % 2 == 0
    .hoge
  # ...どうすればいいのん?(´・ω・`)

Haml を使ったことがある方はわかると思いますが、閉じタグが自動で入れられてしまいます。上記のif 文を閉じる時には同時に<div class=”hoge”> も閉じられてしまいます。

ひとまず力技でやってみる

Haml で書けないならRails で書けばいいじゃない。ということでやってみました。

- items.each do |item|
  - if index % 2 == 0
    = "<div class='hoge'>".html_safe
  .fuga= item.to_s
  - if index % 2 == 1
    = "</div>".html_safe

動いたには動いたけど、格好悪い。

surround を発見し、使ってみた

調べてみたところ、HAML にsurround というメソッドがありました。これはこのメソッドのブロックの前後に任意の文字列を入れることが可能です。

- items.each_with_index do |item, index|
  = surround(index % 2 == 0 ? "<div class='hoge'>".html_safe : "", index % 2 == 1 ? "</div>".html_safe : "") do
    .fuga= item.to_s

出来た!しかも短い!でも大バカヤローでした。

いつからそのERB が最適だと錯覚していた?

社内で上記の話をしてみたところ、each_slice があるじゃないかと突っ込みをくらいました。ERB にすると下記の通りです。

<% items.each_slice(2) do |temp_items| %>
  <div class="hoge">
    <% temp_items.each do |item| %>
      <div class="fuga"><%= item.to_s %></div>
    <% end %>
  </div>
<% end %>

おぉ、これならHaml にも簡単に直せる!

- items.each_slice(2) do |temp_items|
  .hoge
    - temp_items.each do |item|
      .fuga= item.to_s

今度こそ出来た!しかもさっきより読みやすい!きっとこっちの方が良い解決案だと思います。

でもこれより更に良い解決方法はあるかもしれません、知ってる方はコメントください。

ちょっと待った! Railsでgitリポジトリから除外すべきでないファイル:Gemfile.lockとdb/schema.rb

$
0
0

こんにちは、hachi8833です。

Railsをgitで管理するのであれば、ログファイルや、パスワード入りdatabase.ymlなどの登録したくないファイルを.gitignoreに記載してリポジトリから除外するのが普通です。しかし実際の案件では、除外すべきでないファイルが除外されていることがたまにあります。言うまでもないような話ですが、心当たりのある方は念のためチェックしてみましょう。

gitリポジトリから除外すべきでないファイル

以下では、誤ってgitリポジトリから除外されがちなGemfile.lockとdb/schema.rbについて説明します。代表的なものであり、すべてを網羅しているわけではないのでご注意ください。

Gemfile.lock

「GemfileがあればGemfile.lockはなくてもいいんじゃね?どうせbundle installするんだから」または「Gemfile.lockがあるとどうもデプロイがうまくいかない」などの理由でGemfile.lockが除外されていることがありますが、Gemfile.lockは基本的にリポジトリから除外すべきではありません。StackOverflowを見ると、英語圏でも同様の思い違いが頻発しているようです。

Gemfile.lockは、「実際にインストールされたGemセット」のスナップショット+依存関係情報が記載されているファイルです。そしてデプロイ時にbundle installを実行すると、実際に使用されるのはGemfileではなくGemfile.lockです。

bundle installは、状況によって以下のように動作します。

Gemfile.lockがない場合(通常は開発最初期)
Gemfile.lockが作成され、Gemfileに従ってgemがインストールされる。そのときに実際にインストールされたgemとその依存関係がGemfile.lockに保存される。
Gemfileにgemが追加された場合(通常は開発時)
Gemfileに追加されたgemとそれに依存する未インストールgemのみがインストールされ、Gemfile.lockはそれらのgemについてのみ更新される。
Gemfileに変更がない場合(通常はデプロイ時)
Gemfile.lockに従ってインストールされる。Gemfileは参照されず、Gemfile.lockは更新されない。

上の動作からわかるように、開発環境で動作確認されたgemセットを、デプロイ先でもまったく同じように揃えるためにGemfile.lockが使用されています。大事な大事なGemfile.lockがリポジトリにないと、開発時とデプロイ時の間にgemが新しくなった場合にデプロイ側だけgemが新しくなってしまいます。

補足: bundle updateについて

ここで注意すべきはbundle updateです。bundle updateを実行すると現状のGemfile.lockの内容は無視され、上で言う「Gemfile.lockがない場合」と同じことが行われます。当然ながら、Gemfile.lockは、現時点の最新バージョンと依存関係を持つgemによってごっそり更新されてしまいます。

かく言う私も、当初はbundle installbundle updateの違いに気付かないままカジュアルにbundle updateを実行してしまってました。gemの紹介記事などでbundle updateを指示していることが時たまあり、それに釣られてしまっていたようです。恐ろしいことです。

アプリ開発の初期段階では、bundle installとbundle updateのどちらを実行していても矛盾は顕在化しません。しかし、しばらく更新が行われていない間にgemのバージョンが進んでしまうと、bundle updateを実行したときにgemとGemfile.lockが一斉更新されてしまい、以前動作したgemが動作しなくなるようなことがあります。

開発初期のローカルアプリならともかく、実際に動いているシステムのメンテ中にうかつにbundle updateを実行すると死を招きます。そしてGemfile.lockがリポジトリに登録されておらず、前回デプロイ時のバックアップもなければ、動いていた状態を再現できなくなって詰んで終わります。

Gemfile.lockはリポジトリに登録しましょう。そしてbundle updateは基本的に実行せず、特定のgemのみを最新にする目的でbundle update gem名のようにgem名を指定して実行するぐらいにとどめましょう。

なお、Gemfile.lockがあるとデプロイがうまくいかないのは、開発・本番環境間の不整合が原因である可能性が高いので、Gemfile.lockを除外するのではなく不整合を解決する必要があります。

例外

弊社CTOから補足: gemを作るときには逆にGemfile.lockをリポジトリに含めないのが定石です。ライブラリでlockするとえらいことになりますヨ。

db/schema.rb

「db/migration以下のマイグレーションファイルがあればschema.rbいらなくね?どうせrake db:migrateするんだから」などの理由でschema.rbが除外されていることがありますが、これも基本的に除外すべきではありません。

schema.rbはマイグレーションファイルから生成され、Railsは最終的にこのスキーマ情報を使用してデータベースを扱います。言い方を変えれば、マイグレーションはこのschema.rbを作成するために行っているわけです。具体的には、rake db:migrateを実行するとrake db:schema:dumpも実行され、これによってdb/schema.rbが更新されます。

アプリケーションの規模が大きくなってマイグレーションファイルが増えてきたり、マイグレーション/ロールバックを繰り返すうちに、マイグレーションの累積結果とスキーマが必ずしも一致しなくなることがあります。その場合におかしくなっている可能性が高いのはマイグレーションの方であり、頼りにすべきは現状のシステムで動いているschema.rbの方です。

実際、RailsGuidesにもしっかり書いてあります。

Because schema dumps are the authoritative source for your database schema, it is strongly recommended that you check them into source control.

追伸: schema.rb自体にも書いてありました。

# It’s strongly recommended to check this file into your version control system.

マイグレーションファイルを追加する時には、過去のマイグレーションを参照するより先にschema.rbで現状を確認しながら行うぐらいの方が確実です。

また、rake db:resetなどのコマンドはschema.rbに依存しているので、schema.rbがないと実行できません。

gitリポジトリから除外すべきファイル

以下も見落とされがちなものを挙げます。

  • .rvmrcは環境依存するので、よほど特殊な事情がなければリポジトリに登録すべきではありません。
  • database.ymlにはパスワードが記載され、さらにデータベース環境に依存するので、通常は登録しません。代りにそのテンプレートとしてdatabase.yml.sampleを登録しておき、開発開始時にこれをリネームして環境情報とパスワードを記入するという運用が一般的です。

普通除外するファイル

オフィシャルなgitignore / Rails.gitignoreをベースにしておけば大丈夫でしょう。

除外するかどうか時と場合によるファイル

以下も見落とされがちなものを挙げます。

  • tmpディレクトリは通常登録しませんが、たまにアプリがロックファイルなどを置いてたりすることがあるので、その場合はそのファイルを登録します。
  • .bundleは、bundle/configに–pathなどのbundle installのオプションが保存されます。作業者が全員gemのインストール先について承知しているなら登録してもよいですが、そうでなければ混乱を避けるため登録しないようにします。

参考

  1. http://bundler.io/v1.5/bundle_install.html
  2. http://bundler.io/v1.5/bundle_update.html
  3. ツールを使いたいだけの人のための bundler 入門 (例: vagrant + veewee)
  4. RailsGuides / Active Record Migrations / 7 Schema Dumping and You

Unicornがどれくらい仕事してるかさくっと調べる

$
0
0

Unicornのワーカー数を増やすべきか考えるために、今いくつのワーカーがリクエストを捌いているのかさくっと調べたいことがあると思います。
Passengerだとpassenger-statusコマンドが標準で付いていて便利なのですが、Unicornではそうもいきません。

今知りたい場合

たとえば以下のような1行コマンドで調べられます。

ps l --ppid=`cat /path/to/rails/tmp/pids/unicorn.pid` | \
awk "{ print \$10}" | grep R | wc -l

これは、masterプロセスの子プロセスを一覧してステータスがTASK_RUNNINGのものを数えているだけです。
多少雑ですが、watchで数秒おきに見ておけば、負荷状況がざっくりとわかります。

muninで監視する場合

これだけだと短いので、muninで監視する設定も書いておきます。

muninで監視するならプラグインが用意されているのでそれを使うのが簡単です。
GitHubページ

この3ファイルを/usr/share/munin/pluginsにでも置いておいて、symbolic linkを置いて使います。

ln -s /usr/share/munin/plugins/unicorn_ /etc/munin/plugins/unicorn_average
ln -s /usr/share/munin/plugins/unicorn_ /etc/munin/plugins/unicorn_memory
ln -s /usr/share/munin/plugins/unicorn_ /etc/munin/plugins/unicorn_processes
ln -s /usr/share/munin/plugins/unicorn_status /etc/munin/plugins/
ln -s /usr/share/munin/plugins/unicorn_memory_status /etc/munin/plugins/

設定ファイルも書いておきます。

# /etc/munin/plugin-conf.d/unicorn
[unicorn_*]
env.rails_root /path/to/railsroot

その他必要に応じて以下の編集をします。

  • /usr/share/munin/plugins/unicorn_はgrep “unicorn_worker”としている箇所があるので、grep “uincorn_rails worker”に書き換える
  • /usr/share/munin/plugins/unicorn_statusと/usr/share/munin/plugins/unicorn_memory_statusの1行目のRubyのパスを、RVM経由のパスに書き換える

動作確認します。

sudo munin-run unicorn_average
sudo munin-run unicorn_memory
sudo munin-run unicorn_processes
sudo munin-run unicorn_status
sudo munin-run unicorn_memory_status

これで、メモリ使用量やワーカー数をMuninで監視することができます。
ちなみにこのプラグインでは、ワーカーのactive/idleを、「1秒sleepしてCPU timeが進んだかどうか」で判断しているようです。

unicorn-status

途中で設定間違えてグラフが消えていますが、無事監視できました。暇してるなあ。
ちなみに、当然ながらwatchコマンドやmuninの収集間隔より短い突発的な負荷は計測できないのでご注意ください。

Railsのルーティングを極める(前編)

$
0
0

こんにちは、hachi8833です。今回も弊社CTOの馬場さんによる勉強会のスライドを元に記事を書きました。執筆当時はRails3だったので、Rails4情報も追加しました。

Rails routesを極めよう

2012/03
baba

Railsのルーティングはきわめて自由度が高い分、気を付けないとすぐカオスになってしまいます。Railsのルーティングのコツについて勉強していきましょう。Railsのルーティングはconfig/routes.rbで設定します。

とにかくrake routes

ルーティングを書く際にはrake routesを実行する癖をつけましょう。ルーティングを作成するだけでなく、Railsのデバッグ時にも有用で、迷ったらとにかくrake routesを実行してもいいくらいです。

rake routesは実行のたびにRails環境を読み込むので、そのままでは遅いので有名です。以下のような方法で高速化しましょう。

  • pryとpry-rails gemがインストールされているなら、rails consoleを起動しておいて show-routesを実行
  • spring gemがインストールされているならspring rake routesとすれば2度目以降から高速になる
  • dev環境が起動中ならhttp://localhost:3000/rails/infoを参照する (Rails 4)

改めて、RESTとは

RESTとは、Railsに敷かれているレールの一つである概念で、REpresentational State Transferの頭字語です。その特徴は:

  • ステートレスであること
  • すべてを「リソース」で表す
  • リソースは名前を持つ

RESTに従っていることをRESTfulと呼んだりします。

RESTfulなURLの例:

  • http://example.com/prefectures
  • http://example.com/users/1

RESTのちょうど反対の概念がRPC(Remote Procedure Call)です。これはクエリ形式などと呼ばれることもあることからわかるように、?の後ろに問い合わせを&でつないで表現します。

RPC URLの例:

  • http://example.com/index.php?action=prefecture_list&id=1
  • http://example.com/PrefectureList.aspx?id=1 http://example.com/xmlrpc

Railsではresourcesでルーティングを記述することで自動的にRESTfulなルーティングが生成されます。
以下はusersコントローラとproductsコントローラへのルーティングです。

resources :users
resources :products

その場合、対応するコントローラにもRESTfulなアクションが揃っている必要があります。rails generate scaffoldで生成した場合は自動的にRESTfulになります。

Railsでは、REST形式もRPC形式も両方扱うことができますが、RailsはRESTを「レール」として定めていますので、統一のためにRESTfulなルーティングの作成を心がけるようにしましょう。
ただし、RESTはあくまでポリシーであり万能ではないので、時にはRPC形式を一部に導入する方が素直に作れることもあります。

RESTメソッド

RESTとHTTPメソッドにはそれぞれ以下のような関係があります。なお、Rails 4 からはPUTは非推奨となりPATCHが推奨されています。
RESTメソッド:

メソッド 安全 冪等
GET
POST × ×
PATCH/PUT ×
DELETE ×

安全が×になっているのは、危険という意味ではなく、実行すると元のデータが更新されるという意味です。同様に、安全が◯になっているのは、実行によって更新される心配がないという意味です。

冪等(べきとう: idempotent)は近年よく使われる用語で、「1回実行しても2回以上実行しても結果が変わらない」ことを指します。1人殺しても3人殺しても死刑、は冪等です。1人殺せば犯罪者、1000人殺せば英雄、1億人殺せば神、だと冪等ではありません。chefやvagrantなどのサーバーデプロイ用DSLではその目的のため冪等性が重視されます。

以下の表では、BPSという会社の所在地をGETメソッドとRESTfulなURLで表現した場合の例を示しています。

概念 RESTfulなメソッドとURL
都道府県 GET /prefectures
東京都 GET /prefectures/tokyo
東京都市区町村一覧 GET /prefectures/tokyo/cities
東京都新宿区 GET /prefectures/tokyo/cities/shinjuku
東京都新宿区会社一覧 GET /prefectures/tokyo/cities/shinjuku/companies
東京都新宿区にあるBPSという会社 GET /prefectures/tokyo/cities/shinjuku/companies/bps
BPSという会社 GET /companies/bps

以下の表では、記事・ユーザ・コメントを表現した場合の例です。特に、IDが複数ある場合の表現方法にご注目ください。

概念 RESTfulなメソッドとURL
記事一覧 GET /articles
記事(ID=1)、コメント一覧 GET /articles/1/comments
記事(ID=1)、コメント(ID=1) GET /articles/1/comments/1
ユーザ(ID=1) GET /users/1
ユーザ(ID=1)、パスワード GET /users/1/password

以下の表では、記事・ユーザ・コメントに対して操作を行なう場合の例です。Rails 4ではPUTは非推奨になり、PATCHが推奨されます。

概念 RESTfulなメソッドとURL
記事を投稿する POST /articles
記事(ID=1)にコメントを投稿する POST /articles/1/comments
記事(ID=1)を更新する PATCH /articles/1
記事(ID=1)のコメント(ID=1)を更新する PATCH /articles/1/comments/1
記事(ID=1)を削除する DELETE /articles/1
ユーザ(ID=1)のパスワードを更新する PATCH /users/1/password
(エラー) POST /users/1/password

ルーティングを綺麗に書くコツ

  • RESTに従う
  • 原則として、RESTに従うようにしましょう。頑張ればresourceですべて書くことができます。が、原理主義的にがんばるとかえって見通しが悪くなることもありますのでほどほどにしましょう。

  resources :admin_menus, only:[ :index, :update ]
  resources :menus, only:[ :index ]
  resources :deadlines, except:[ :create, :destroy ]
  • 以下の名前を理解する
  • リソース
    HTTPメソッド(GET/POST/PATCH/PUT/DELETE)で操作する対象となるURLです。
    名前付きルート
    「パス_path」(ドメイン名より下のパスのみ)または「パス_url」(httpなどから始まるフルパス)という形式でURL形式を指定できます。たとえばContactページが/contactというパスにある場合、contact_pathまたはcontact_urlという名前付きルートを使用して指定できます。パスヘルパーやURLヘルパーとも呼ばれます。これにより、コード上でURL構造を意識せずにパスを指定できます。

    なお似ているけど違うのは「名前付きスコープ」と「名前付きパラメータ」です。

    ネストしたリソース
    ネストしたルーティングを記述することで、あるリソースを他のリソースの子にすることができます。
  • ファイル分割を検討する
  • Railsアプリケーションが成長してルーティングが膨大になったら、config/routes.rbの分割を検討しましょう。たとえば以下のようにconfig/application.rbに記載することで、config/routes.rbを分割してconfig/routes/以下のroutes_1.rbとroutes_2.rbに置くことができます。

    config.paths["config/routes"] << "config/routes/routes_1.rb" 
    config.paths["config/routes"] << "config/routes/routes_2.rb" 
    
  • アルファベット順にソートする
  • config/routes.rbは記載順にマッチしますので、上にあるものが優先されます。しかし、膨れ上がったルーティングを人間が管理する際には、アルファベット順に並べておくのはひとつの方法です。

    ネストしたリソースなどについては、後編に続きます。

    Ruby on Rails 3.2.17, 4.0.3がリリースされました!重要なDoS脆弱性が修正されています

    $
    0
    0

    日本時間で2/19の朝、Ruby on Rails 3.2.17, 4.0.3, 4.1.0beta2がリリースされました
    今回は、重要なセキュリティFixを含む一斉アップデートです。

    なお、4.1についてはすでに4.1.0rc1がリリースされているので、そちらにアップデートしましょう。

    今回は3件の修正(3系でのみ発生するものが1件、4系でのみ発生するものが1件、両方で発生するものが1件)が含まれています。
    特に最後のDoS脆弱性(CVE-2014-0082)については、Rails 3.2を使用している幅広いサービスに影響があるため、確実に確認・アップデートしておきましょう。

    CVE-2014-0080

    影響を受けるRailsバージョン: 4.0, 4.1

    ActiveRecordでPostgreSQLの配列型を使っている際の、データインジェクション脆弱性です。
    レコードを削除したり任意のSQLを実行できる性質のものではありませんが、アプリケーションの動作に影響を及ぼすデータを注入される可能性があります。

    PostgreSQLの配列型を使っていない場合は、この脆弱性の影響は無いので心配ありません。
    Rails 3.2系もこの影響を受けないので、Rails 4系を使っている場合のみ対策が必要です。

    もしアップデートできない場合、個別パッチが提供されているので適用しましょう。

    https://groups.google.com/forum/#!topic/rubyonrails-security/Wu96YkTUR6s

    個人的にはPostgreSQLをあまり使わないので、詳細確認していません。

    CVE-2014-0081

    影響を受けるRailsバージョン: 3.2, 4.0, 4.1

    number_to_currency, number_to_percentage, number_to_humanのXSS脆弱性です。
    formatパラメータがHTMLエスケープされないため、formatをユーザが指定できる環境ではXSSが成立します。

    たとえば以下のようなコードです。

    number_to_currency 50000, format: params[:format]
    

    Rails 3.2, 4.0, 4.1すべてが影響を受けるので、アップデートするか、個別パッチを当てるか、formatを手動でエスケープしましょう。

    個別パッチはこちらです。

    https://groups.google.com/forum/#!topic/rubyonrails-security/tfp6gZCtzr4

    ちなみに、アップデートしても、localeファイルで指定しているHTMLタグ付きformatはエスケープされないので、一般的には困らないと思います。

    # config/locales/en.yml
    # このような指定をしていても、アップデート後に特に修正は不要です
    en:
      number:
        currency:
          format:
            format: "%n%u"
            unit: "<b>Yen</b>"
            separator: "."
            delimiter: ","
            precision: 0
    

    CVE-2014-0082

    影響を受けるRailsバージョン: 3.2

    ActionViewでrender :textを使った際のDoS脆弱性です。
    以下のようなコードはよくありますが、これでDoS攻撃が成立します。

    class HogeController
      def hello
        render text: 'Hello world!'
      end
    end
    

    HTTPリクエストのAcceptヘッダに応じてformatsが決定されますが、これは:htmlや:text, :jsのようなsymbolに変換されます。
    ここで、Acceptヘッダに長い文字列を少しずつ変えながら大量にリクエストすると、symbolが大量生成されることになります。
    symbolはGCで回収されないので、これだけで、OOM Killerが発動するまでメモリ使用量を無限に増大させることができ、DoS攻撃が成立します。

    試してみましょう。
    まず、わかりやすくするためにシンボル数をレスポンスに含めます。

    def hello
      render text: "Hello world!, symbols=#{Symbol.all_symbols.count}"
    end
    

    また、メモリ使用量を監視しておきます。

    while true; do ps alx | grep rails | grep -v grep | awk '{print $8}'; sleep 2; done
    

    試しにブラウザからアクセスしてみたら、初回はシンボル数15,166、メモリ使用量(RSS) 56,840でした。

    次に、Acceptヘッダに長い文字列(とりあえず1KB程度)を入れて、毎回微妙に変えながら1万回ほどアクセスしてみます。
    (念のため具体的なコードは避けますが、Rubistなら1行で書けるはずです)

    メモリ使用量はこんな感じになりました。
    途中GCが走って減る箇所もありますが、トータルでかなり増えてしまっています。

    56840
    56840
    66476
    72924
    73900
    76940
    78980
    80408
    82708
    (略)
    246264
    247560
    248532
    249304
    251288
    253240
    

    また、シンボル数も25,164まで増えていました。要するにアクセス回数分増えています。

    このペースだと、10分とかからずOOMを発生させることができそうです。

    Rails 4.0系ではこの影響を受けないので、Rails 3.2を使っている場合のみ対策が必要です。

    個別パッチはこちらです。

    https://groups.google.com/forum/#!topic/rubyonrails-security/LMxO_3_eCuc

    まとめ

    未だRails 3.2は広く使われていることを考えると、(CVE-2013-6414でも似たようなことをやっていた気がしますが)CVE-2014-0082のDoS脆弱性は非常にインパクトがあります。
    アップデートは早急に!

    ルーティングを極める (後編)

    $
    0
    0

    こんにちは、hachi8833です。「ルーティングを極める」の後編です。今回はRails 4.0.3 + Ruby 2.1.1の環境で動作確認しています。
    (ルーティングを極める(前編))

    Rails routesを極めよう

    2012/03
    baba

    resourcesとネスト

    Railsのルーティング記法の基本は、resourcesとresourceです。また、Railsのルーティングにはネストを含む多くのオプションがあり、自由度が飛躍的に高まっています。

    以下の2つのルーティングは、ネストしていない単純なresourcesルーティングです。prefectures、articlesいずれも、コントローラに合わせて複数形で書く点にご注意ください。

    resources :prefectures
    resources :articles

    rake routesしてみると、prefecturesとarticlesそれぞれについてRESTfulかつ標準的なアクション(index/create/new/edit/show/update/destroy)を網羅したなルーティングが一気に生成されています。

    nonnested

    ところで、せっかくなのでRails 4.0のルーティング表示機能でも見てみましょう。development環境でRailsを起動して/rails/info/routesを参照します。名前付きルートもHelper列にわかりやすく表示され、[Path]と[Url]をクリックすれば_pathと_urlを切り替えて表示するという細かい芸もやってくれています。

    nonnested_4.0

    なお名前付きルートのうち、_pathはドメイン名から下のパス、_urlはhttp://などから始まるフルパスであることは前編でも説明いたしました。生成された名前付きルートをrails consoleで確認するには、app.に続けて名前付きルートを入力してみます。

    namedroutes

    複数形にはidはなく、単数形にはidがある、と覚えておくとよいでしょう。コードで名前付きルートを使用することで、生々しいパスをコードで書かずに済みます。名前付きルートはカスタムで指定することもできますが、なるべくこのように標準的なものをRailsに生成させる方が楽ですし混乱せずに済みます。

    では、このうちprefecturesの下にcitiesとcompaniesをネストさせてみましょう。

    resources :prefectures do
      resources :cities do
        resources :companies
      end
    end

    このときのルーティングテーブルは以下のようになります。
    nested

    見てのとおり、prefecturesのルーティングに加え、prefectures/cities、そしてprefectures/city/companiesという階層が追加されました。いずれも複数形の「resources」を指定しているので、prefectures, city, companyにはidがあります。

    このときの名前付きルートは次のようになります。idの部分には適当な数字を入れてあります。
    nestedroutes2

    単数リソース

    上では複数形のresourcesを使用してid付きのルーティングを生成しましたが、単純なページへのルーティングのようにidが不要な場合は単数形のresourceを指定することができます。

    仮にprefectureでidが不要だとすると、以下のように単数形のresourceを指定し、prefectureも単数で書きます。

    resource :prefecture

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

    resource

    見てのとおり、Pathにid:が含まれなくなり、indexアクションもなくなりました。なお、この記述「resource」も「prefecture」も単数ですが、これによって指定されるコントローラは「prefectures」と複数形になっていることにご注意ください。御存知のとおり、Railsではコントローラ名を複数形で書くことになっています。

    名前付きルートを確認します。なお、無効なはずのidをわざと付けてみると、妙なパスが生成されました。
    singleresource

    複数リソースと単数リソースのネスト

    今度は複数リソースと単数リソースの組み合わせの例を示します。ユーザーは複数いるのが普通なのでidを指定しますが、ユーザーごとのパスワードは1つしかないのが普通なので、パスワードではidを指定しない、という状況です。

    この場合以下のようなルーティングが考えられます。外側のusersは複数リソース、内側のpasswordは単数リソースです

    resources :users do
      resource :password
    end

    この場合は以下のルーティングが生成されます。

    combinednest

    期待どおり、userにはidがあり、passwordにはidがありません。
    なお、user_pathのようにそのリソースがパスの最後尾にある場合にidは「/:id」と表記されていますが、user_password_pathのようにuserがパスの途中にある場合では「/:users_id/」と表記されています。どちらもidです。

    nestedsingle

    resourceベースでないルーティングの書き方

    resourceベースでない、HTTPメソッドを指定したルーティングも可能です。その方がresourcesよりもルーティングテーブルがシンプルになるのであれば使う方がよいと思います。

    get 'hello1', to: 'pages#hello'
    get 'hello2', :controller => 'pages', :action => 'hello2' 
    get 'hello3/:id', to: 'pages#hello3'
    post 'hello4', to: 'pages#hello4'

    以前は、get/post/put/update/deleteすべてのHTTPメソッドにマッチさせたいときはmatchを使ったのだそうですが、ワイルドカードはセキュリティ上だらしなくなりやすいため、Rails 4からvia:オプションなしでのmatch指定は禁止されています。

    match :hello5, to: 'pages#hello5' #禁止 (エラーが表示される)
    match :hello5, to: 'pages#hello5', via: [:get, :post] #許される

    さらに、かつては以下のように書くことができたのだそうです。

    match ':controller(/:action(/:id))(.:format)'

    こうするとコントローラ・アクション・idが実在さえしていればupdateやdestoryなど何にでもマッチしてしまうという、楽ちんかつ風通しの良すぎるルーティングになります。このヒューヒューの全通しルーティングは当時から危険視されていたらしく、チュートリアルや実験用以外で使うべきでないとされていたようですが、今は完全に禁止されています。

    namespace

    Railsのルーティングでは以下のようにnamespaceを指定してパスをグループ化することができます。これを使用して、たとえば管理用ページ(admin)のパスや置き場所を仕切ることができます。

    namespace :page do
      get :privacy_policy
      get :company_information
      get :term_of_use
      get :businessdeal
    end

    namespaces1

    上のように、名前付きルートとパスとコントローラ#アクションにpageが追加されました。

    上は素朴なgetメソッドルーティングでしたが、resourcesルーティングに名前空間を与えることもできます。

    namespace :admin do
      resources :users
    end

    namespaces2

    同じく、名前付きルートとパスとコントローラ#アクションにadminが追加されました。

    :moduleオプションを使用してresourcesに名前空間を与えることもできますが、これは上と少し動作が異なります。

    resources :users, module: :admin
    
    #以下も同等
    scope module: :admin do
      resources :users
    end

    namespace3

    こちらはコントローラ#アクションにしかadmin名前空間が追加されていません。ここからわかるように、パスには表したくないが別のディレクトリにまとめたいコントローラがある場合に使用できます。

    collectionとmember

    既に見たように、resourcesを使用すれば主要な7つのルーティングが自動的に追加されますが、 それ以外のルーティングをそのリソースに追加したい場合はmemberまたはcollectionを使用します。
    さっきの「複数形はidなし、単数形はidあり」と同じ考え方で、「collection(集合)はidなし、member(個別)はidあり」と覚えましょう。以下のルーティングを例に取ります。

    resources :books do
      collection do
        post :search
        post :remove_multi
      end
      member do
        get :thumbnail
        get :sample_file
      end
    end

    これをルーティングテーブルにすると以下のようになります。

    collectionmember

    見てのとおり、いつもの7つのルーティングに加えて4つのルーティングが追加されています。そしてcollectionで指定した2つにはidはなく、memberで指定した2つにはidがあります。

    なお、名前付きルートはこの場合books_search_pathとかではなくsearch_books_pathのように上位のリソースが後になっていることにご注意ください。一応名前付きルートも確認してみましょう。

    collectmember

    以下のような簡略版表記も使用できます。

    resources :books do
      post :search, on: :collection
      get :thumbnail, on: :member
    end

    ここで1つ注意があります。collectionもmemberも指定せずに書いた場合はデフォルトで「member扱い」となります。

    resources :books do
      post :search
      post :remove_multi
    end

    上の2つのリソースのルーティングテーブルを見てみると、確かにidが含まれており、member扱いされていることがわかります。

    nocollectmember

    root

    今更ですが、rootへのルーティングの書き方は以下のとおりです。これだけ他の書き方と比べて少し浮いている感じですね。

    root to: 'page#top'

    オプション指定

    最後に、ルーティングでよく使われるオプションを紹介します。

    onlyとexcept

    resourcesでonlyまたはexceptオプションを使用することで、主要な7つのアクション(index, show, new, create, edit, update, destroy)を限定することができます。updateやdestroyのような破壊的なアクションはルーティングレベルで塞ぐようにしておきましょう。

    # indexとshowアクションだけ使用する場合
    resources :prefectures, only: [:index, :show]
    # destory アクション以外を使用する場合
    resources :prefectures, except: :destroy 
    

    リソース名の変更

    asオプションを使用して、リソース名を変更することができます。

    get 'home', controller: :users, as: 'user_root'

    asoption

    httpsの指定

    以下のようにhttpsを指定することができます。

    scope protocol: 'https://', constrains: {protocol: 'https'} do
      root to: 'page#top'
    end
    

    idを拡張

    たとえば、以下のようにidの制約を変更してアルファベットのidを使用することができます。

    resources :prefectures, id: '/^[a-z]+$/'
    

    最後に

    Railsには他にも強力なルーティングのオプションがたくさんありますが、アドホックなカスタムルーティングを避け、なるべくresouces/resourceとonly/exceptで素直かつ統一のとれたルーティングを生成するようにします。コントローラが数百にのぼる巨大なルーティングをすべてresources/resourceで書いた例もあります。
    ただし、そこでRESTfulにしようと頑張り過ぎないのも大事です。

    参考

    Rails4でサイト構築をする – Rails環境構築編

    $
    0
    0

    [Rails4でサイト構築をする]
    Rails環境構築編
    Scaffold利用編
    – Bootstrap導入編
    – WYSIWYG導入編
    – CSV出力機能編
    – スクレイピング機能編(nokogiri)
    – 非同期処理導入編(delayed_job)
    – デプロイ環境構築編(capistrano3)
    上記を毎週1つずつ出す予定

    Ogawaです。Android生活から4か月ぶりくらいに戻って、Rails4を使ってサイト構築をしたので、その時の作業をまとめて記事にします。
    #作ったのは自社のSEO業務を簡略化するWebシステムサイトです。
    まずは復習もかねてRVM, Ruby, Railsをインストールするところから始めます。
    # 私はrbenvよりrvmのほうが使い慣れているので、rvm使ってます。
    # BPSに今後入ってくる人のために、Rails環境の構築から書いています。

    [前提環境]
    Ubuntu 12.04

    RVMのインストールと設定

    http://rvm.io/rvm/install

    ↑を参考にインストールします。Rubyもインストールしちゃいたいので以下のコマンド選択。

    curl -sSL https://get.rvm.io | bash -s stable --ruby
    

    [結果]
    The program ‘curl’ is currently not installed. You can install it by typing:
    sudo apt-get install curl

    エラーがでました。curlをインストールしろと出ているので、インストールします。

    sudo apt-get install curl
    

    [結果]
    tech1
    何かエラーが出ました。。。
    apt-get updateしろって言っているので、やってやります。
    その後、もう一度curlのインストールをします。
    今度は成功しました。

    curl --vesion
    

    を実行すると結果が出てきます。
    [結果]
    curl 7.22.0 (i686-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
    Protocols: dict file ftp ftps gopher http https imap imaps ldap pop3 pop3s rtmp rtsp smtp smtps telnet tftp
    Features: GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP

    curlがインストールできたので、一番最初のコマンドでrvmをインストールします。
    #何回かユーザパスワードの入力を求められます。

    rvmのバージョンを確認してみます。その前に環境変数を読み直します。

    source .bashrc
    
    rvm --version
    

    [結果]
    rvm 1.25.19 (stable)

    インストールされたRubyの確認

    rvm list
    

    [結果]
    =* ruby-2.1.1 [ i686 ]

    .bashrcに以下を追記します。

    source "$HOME/.rvm/scripts/rvm"
    

    #これをやらないとrvm useとかしたときにエラーになります。
    ※環境変数の読み直しはもちろんやってください。

    チームで作業する場合は環境による差異を少なくするためにRubyのバージョンは統一するので、
    指定のRubyが入っていないなんてことはよくあります。
    なので、今回は ruby 2.0.0をインストールしてみます。

    rvm install 2.0.0
    
    rvm list
    

    [結果]
    rvm rubies
    ruby-2.0.0-p451 [ i686 ]
    =* ruby-2.1.1 [ i686 ]
    # => – current
    # =* – current && default
    # * – default

    ruby 2.0.0を使ってみます。

    rvm use 2.0.0
    

    # 2.0.0をデフォルトにする場合は–defaultを付ける。

    Gemsetの作成

    rvm gemset create tecracho #techrachoはプロジェクト名
    

    gem install bundler

    [結果]

    bundle -v
    

    Bundler version 1.5.3

    Railsのインストールとプロジェクト作成

    Railsのインストール

    gem install rails
    

    [結果]

    rails -v
    

    Rails 4.0.3

    ※既にRailsが入っていてバージョンが古い人は

    gem update rails
    

    で新しいバージョンが入ります。

    Railsプロジェクトの作成

    rails new techracho #techrachoはプロジェクト名
    

    [結果]
    tech2

    .ruby-versionと.ruby-gemsetの作成
    プロジェクト変わるごとに、コマンドを打ってRubyのバージョンやGemsetを変えるのは大変なので、
    ↑の2ファイルをプロジェクトフォルダ直下に作成しておきます。
    その2ファイルを作成することで、プロジェクトフォルダに移動したときに自動で指定したRubyバージョンとGemsetになるようにします。
    .ruby-version
    ruby-2.0.0

    .ruby-gemset
    techracho

    bundle installしてサーバ起動

    bundle install
    
    rails server -p 8080 #-pでポート指定
    

    tech3

    JavaScriptランタイムがないとのことなので、入れてあげる。
    Gemfileに以下を追加。

    gem 'therubyracer'
    

    再度、コマンドを実行して、起動します。
    ブラウザからlocalhost:8080をたたいて「Welcome aboard」と書かれたページが出ていれば成功です。

    Unicornのインストールと起動

    Gemfileに以下を追加します。

    gem 'unicorn'
    

    bundle installして以下を実行

    unicorn_rails -p 8080
    

    まとめ

    今回は、Railsサーバーを起動するところまでをやりました。
    改めてやってみて、思ったのは、意外に時間がかかるな~ってところです。
    (主にインストールが)
    次回は、Scaffoldを使ってコントローラーとかモデルとか作ってみたいと思います。

    Viewing all 1384 articles
    Browse latest View live