スコープを使ってアクセスする
元記事はこちら
翻訳
オブジェクトのオーナーとcurrent_userを比較することで、アクセス権をチェックしたいとおもったら、そんな冗長でイケてない書き方はせずに、スコープをつかてアクセスしましょう。
Bad Smell
class PostsController < ApplicationController def edit @post = Post.find(params[:id]) if @post.user != current_user flash[:warning] = 'Access denied' redirect_to posts_url end end end
上記の例では、postのuserをcurrent_userと比較して、一致しなかった場合に、postの編集を許可しないようにしている。 しかし、このやり方は、edit,update,destroyなどを実行できるかチェックするには冗長すぎる。スコープを使ってリファクタリングしよう。
Refactor
class PostsController < ApplicationController def edit # raise RecordNotFound exception (404 error) if not found @post = current_user.posts.find(params[:id]) end end
こうすることで、current_user.postsにだけ紐づくpost、言い換えるとcurrent_userに紐づくことが保証されたpostを検索することができた。 紐付いたpostが存在しない場合、404エラーが返却される。 current_userとオーナーを比較する必要は全くなく、スコープアクセスを使うだけでアクセス権のチェックをより単純に書くことができる。
感想
404が返るってところは、厳密に言うとActiveRecord::RecordNotFoundがraiseされて、rescue_fromを書いていれば、ActionController::Rescueがひろう仕組みらしい。
Modelの関連付けを利用する
元記事はこちら
翻訳
Bad Smell
class PostsController < ApplicationController def create @post = Post.new(params[:post]) @post.user_id = current_user.id @post.save end end
この例では、user_idは明らかに@postに割り当てられます。大きな問題にはなりませんがModelの関連付けを利用すれば、この行は書かずに済みます。
Refactor
class PostsController < ApplicationController def create @post = current_user.posts.build(params[:post]) @post.save end end class User < ActiveRecord::Base has_many :posts end
userとpostの間に1対多の関連付けを定義したので、current_user.posts.build または current_user.posts.create によってpostを生成することができますし、current_userのidはactiverecordによって自動的にpostのuser_id属性に割り当てられます。
感想
動かすには
class Post < ActiveRecord::Base belongs_to :user end
が必要かな。
正直このへんはよくわかっていないので整理。
Active Record の関連付け (アソシエーション) | Rails ガイド
アソシエーション | 関連 | 特徴 |
---|---|---|
belongs_to | 1:1 | 外部キーを持つ方のモデルに設定する。 |
has_one | 1:1 | 外部キーを持たれる方のモデルに設定する。 |
has_many | 1:n | 外部キーを持たれる方のモデルに設定する。 |
has_many :through | n:n | :through 以下には中間テーブルの名前が入る |
has_one :through | 1:1 | :through 以下には中間テーブルの名前が入る |
has_and_belongs_to_many | n:n | 中間テーブルのmodelを持たない。(DBにテーブルは必要) |
リレーションシップのモデルそれ自体を独立したエンティティとして扱いたい(両モデルの関係そのものについて処理を行いたい)のであれば、中間に結合モデルを使用するhas_many :throughリレーションシップを選ぶのが最もシンプルです。リレーションシップのモデルで何か特別なことをする必要がまったくないのであれば、結合モデルの不要なhas_and_belongs_to_manyリレーションシップを使用するのがシンプルです
ふむ。関連テーブルをmodelから意識する必要がなければhas_and_belongs_to_many
を使うと。
検索処理をスコープに入れる
元記事はこちら
翻訳する
BadSmell
class PostsController < ApplicationController
def index
@published_posts = Post.find(:all, :conditions => { :state => 'published' },
:limit => 10,
:order => 'created_at desc')
@draft_posts = Post.find(:all, :conditions => { :state => 'draft' },
:limit => 10,
:order => 'created_at desc')
end
end
コントローラー内で公開済み(published_posts)と下書き(draft_posts)の検索をするために、込み入った検索処理が書かれているが、以下の2点がよくない。
- コントローラー内に込み入った検索処理を書いてしまうと、たいていの場合長くなってしまい、読みづらい。
- 同じような込み入った検索処理は、他のコントローラーにも存在するだろうし、ロジックを変更させたくなった場合に、複数の箇所に手を入れないといけないという作りは、バグの温床になる。
Refactor((~3.0.9。Rails3.1~ はnamed_scope -> scope))
class PostsController < ApplicationController def index @published_posts = Post.published @draft_posts = Post.draft end end class Post < ActiveRecord::Base named_scope :published, :conditions => { :state => 'published' }, :limit => 10, :order => 'created_at desc' named_scope :draft, :conditions => { :state => 'draft' }, :limit => 10, :order => 'created_at desc' end
込み入った検索処理をコントローラーからモデルに移動した。ご覧の通り、コントローラーのコードが非常にシンプルになった。 込み入った検索処理はモデルにだけ存在するので、ロジックが変わったら、モデル内のコードを変更するだけでいい。
updated: Rails3以降では、named_scopeの代わりにscopeを使いましょう。
感想
scopeだとこんな感じかな
class Post < ActiveRecord::Base scope :published, :conditions => { :state => 'published' }, :limit => 10, :order => 'created_at desc' scope :draft, :conditions => { :state => 'draft' }, :limit => 10, :order => 'created_at desc' end
limit/orderの意味上の役割にもよるけど、ここを別scopeにして、mergeしてもいいのかな。
class PostsController < ApplicationController def index @published_posts = Post.published.view_limit @draft_posts = Post.draft.view_limit end end class Post < ActiveRecord::Base scope :published, -> { where(:state, "published" } scope :draft, -> { where(:state, "draft" } scope :view_limit, -> :conditions => :limit => 10, :order => 'created_at desc' end
Rails Best Practiceを読む
転職してPHP->Ruby(Rails)に宗旨替えしました。
Railsの書き方にようやく慣れてきたものの、まだまだ書き方で指摘されることが多いので、 RailsのBestPracticeを読んで勉強することにしました。 1日1本目標に読んでいく。
Symfony Best Practice - 実践編 Chapter6,8-
前回
いじった結果は随時GitHubにあげていきます。
Chapter6はテンプレートについてです。 Symfony2で作ったTwigのフィルターをSymfony3で使う形にしてみようと思います。
最終的な形は以下です。
Twigエクステンションを作る
ベストプラクティスに従って作ってみます。 まずは、エクステンションのクラスを作ります。
- コンストラクタでメイン言語のtranslatorと併記する言語(sub locale)をインジェクトします。
- getFilters()/getName() は自前のエクステンションの情報を返せるようにオーバーライドします。
- multiTrans()でメインの処理を書きます。
https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-32cc6ad49a0083779d7ee61592a42aa3
localeをいじるためのEventListenerを書きます。本家のまんまコピーです。
https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-4968cc448d20825bee969ece9686fffa
次にサービスに登録します。この時EventListenerも登録してます。
https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-1728bfc1c274f341afc1a0275fca694d
ここまでで自作エクステンションの追加が出来ました。
画面から使うために翻訳ファイルに手を入れます。 今回はわかりやすいようにヘッダーの「Symfony Demo」を併記してみます。
翻訳ファイルに「Symfony Demo」の対訳を追加します。
https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-1848a926db6eb9fbe0e97b401f58d947
最後にTwigにフィルターを適用します。
https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-a2f33b79364623e3cb2737239c5eee62
実行結果
無事併記されました!