rake task, cron,whenever ③(update action実装)

rake task, cron,whenever②(https://subaru-hello.hateblo.jp/entry/2021/09/11/205910)の続きです。

今回学習した内容

・assign_attribute

・gem "pundit"

・Fat controllerの防ぎ方

article.rbの書き方

・ Points

Updateアクションで値を一旦確認。

article_controller.rb


def update
  authorize(@article)                                        #ポイント1
   if @article.assign_attributes(article_params)             #ポイント2
    @article.assign_publish_state unless @article.draft?     #ポイント3
    @article.save!
    flash[:notice] = '更新しました。'
    redirect_to edit_admin_article_path(@article.uuid)
  else
    render :edit
  end
end

ポイント1 authorizeについて

Punditというgemを使うことで、権限機能を付与することができました。

それをコントローラで定義することによって、権限の有無によってarticleが使えるか否かを判断するロジックが出来ました。

Rubyのようなオブジェクト指向プログラミングは、単一責務の原則が採用されています。

(https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html)

Punditはコードの可読性をあげ、スケーラブルな管理権限機能を提供することができると書いてあります。

Punditの具体的な使い方は以下の通り。

  1. ApplicationPolicyを定義
  2. ApplicationControllerでPunditを読み込む
  3. ApplicationPolicyを継承したPolicyクラスを作成
  4. Controllerでauthorize(policy)を使用、権限によってCRUD等の管理をする
  5. viewで呼び出す

ApplicationPolicyを定義

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope
    end
  end
end

ApplicationControllerでPunditを読み込む

これで今後ApplicationControllerを継承したコントローラ内でもPunditが使えるようになりました。

class ApplicationController < ActionController::Base
  include Pundit

ApplicationPolicyを継承したPolicyクラスを作成

class PostPolicy < ApplicationPolicy
  def update?
    user.admin? or not record.published?
  end
end

コントローラ内にPostクラスのインスタンスがあれば、以下のようにできます。

def update
  @post = Post.find(params[:id])
  authorize @post
  if @post.update(post_params)
    redirect_to @post
  else
    render :edit
  end
end

policyに書いた内容を下記のようにすれば簡単にviewに呼び出すこともできます。

class DashboardPolicy < Struct.new(:user, :dashboard)
  def show?
    # ... 
  end
end
Controllers:

authorize :dashboard, :show?

このようにviewに呼び出すことで、「権限がない人には表示されないようにする」記述をスッキリ書くことができました。

Views:

<% if policy(:dashboard).show? %>
  <%= link_to 'Dashboard', dashboard_path %>
<% end %>

ポイント2 assign_attributes (article_params)を使う理由

assign_attributes(article_params)は、article_paramsに入っている複数の値を一気に更新するメソッドで、一見update(article_params)と似ていますが、DBに保存されるか否かと言う点が相違しています。

assign_attributes はDBに保存しない

update はDBに保存する。

updateを使った場合

article_controller.rb


def update
  authorize(@article)
   if @article.update(article_params)                      #updateでDBにアクセス
    @article.assign_publish_state unless @article.draft?
   // draft?はenumで設定したメソッド
    @article.save!                                         #saveでまたDBにアクセス
    flash[:notice] = '更新しました。'
    redirect_to edit_admin_article_path(@article.uuid)
  else
    render :edit
  end
end

updateとsaveで2回DBにアクセスしています。

assign_attributesと言うメソッドをしようして欲しいデータが配列に入った場合にのみDBにアクセスされるようにします。

assign_attributeとは

Rails 3.1 beta1からActive Recordに加えられたメソッド。

特定のattributeを変更するためのメソッドです。

article_controller.rb


def update
  authorize(@article)
   if @article.assign_attributes(article_params)           #DBにアクセスしない
    @article.assign_publish_state unless @article.draft?
    @article.save!                                         #save!で初めてDBにアクセスする
    flash[:notice] = '更新しました。'
    redirect_to edit_admin_article_path(@article.uuid)
  else
    render :edit
  end
end

saveとupdateそれぞれでDBにアクセスしてしまうと、処理に負担がかかって重くなってしまう。

なので、基本的にsaveとupdateは同じメソッド内で使わないのがベターになります。

ポイント3 assign_publish_stateについて

article_controller.rbには「公開待ち状態の記事」か「公開状態の記事」かを判別するための条件分岐文をかく。

もし現在時刻より公開日時が先だった場合は「公開待ち」にし、現在時刻と同じもしくは過去のものだった場合は「公開」にするメソッド。

article_controller.rb


if @article.assign_attributes(article_params)
  unless @article.draft?
    @article.state = if @article.published_at <= Time.current
                       :published
                     else
                       :publish_wait
                     end
  end
  @article.save!
  flash[:notice] = 'Has been updated'
  redirect_to edit_admin_article_path(@article.uuid)
else
<--省略-->
end

このままでも問題はないが、上記の書き方だと、ifの中にifをネストして書いていて、かつunlessも組み合わさっています。

かなり可読性が低い書き方になっている。これをFat controllerと言うらしいです。

@article.state = if @article.published_at <= Time.current
                       :published
                     else
                       :publish_wait
                     end

この部分をもっとシンプルにしてコントローラをスッキリさせたいですね。

新しくモデルにassign_publish_stateというメソッドを定義して、コントローラで呼び出せるようにしていきます。

article_controller.rb


if @article.assign_attributes(article_params)
  @article.assign_publish_state unless @article.draft?
  @article.save!
  flash[:notice] = 'Has been updated'
  redirect_to edit_admin_article_path(@article.uuid)
else
<-- 省略-->
end

app/model/article.rb


def assign_publish_state
  self.state = if self.published_at <= Time.current
                     :published
                   else
                     :publish_wait
                   end
end

かなりスッキリになりました。

まとめ

assign_attribute

特定のattributeを変更するためのメソッド。

バリデーションをかけて、paramsに入っている値が期待する値かどうかを判断したい時に使う。

gem "pundit"

権限があるかどうかを少ないコード(admin?,edit?など)で実装できるgem

Fat controllerの防ぎ方

ビジネスロジックやプレゼンテーションロジックをコントローラに書かないこと。

Rakeタスクの実装 - Qiita

ActiveModel::AttributeAssignment

rails learning rake task, cron, whenever

ActiveRecord の attribute 更新方法まとめ - Qiita

Fat Controllerをコードで予防する方法はないか?