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点がよくない。

  1. コントローラー内に込み入った検索処理を書いてしまうと、たいていの場合長くなってしまい、読みづらい。
  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

Symfony Best Practice - 実践編 Chapter6,8-

前回

hanahirodev.hatenablog.com

いじった結果は随時GitHubにあげていきます。


Chapter6はテンプレートについてです。 Symfony2で作ったTwigのフィルターをSymfony3で使う形にしてみようと思います。

最終的な形は以下です。

github.com

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

実行結果

f:id:hiroyuki-hanai:20161127163547p:plain

無事併記されました!

Symfony Best Practice - 実践編 Chapter5-4-

前回

hanahirodev.hatenablog.com

いじった結果は随時GitHubにあげていきます。


フックを使う

ドキュメントに沿って確認してみよう。 まずはdemoアプリですでに登録されているイベントリスナーを確認する。

ロケールの切り替えを行うイベントリスナーが登録されている。 イベント一覧によると、Requestイベントをフックしている。

// AppBundle\EventListener\RedirectToPreferredLocaleListener
    public function onKernelRequest(GetResponseEvent $event)

サービスとして登録されているか確認。

# /app/config/service.yml
    app.redirect_to_preferred_locale_listener:
        class: AppBundle\EventListener\RedirectToPreferredLocaleListener
        arguments: ['@router', '%app_locales%', '%locale%']
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

イベントのフックにはListenerとSubscriberを使う方法があるみたい。 ドキュメントによると、

  • Subscriberはイベントに関する知識をクラスではなく、サービス定義に集約できるので、再利用性に優れている。(Symfony内部ではこっちを使っている)
  • ListenerはバンドルごとにON/OFFの切り替え設定ができるので、柔軟性に優れている。

ユーザーの権限チェックとかアプリ全体で使うようなチェックはSubscriberで処理するのが良さそう。