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の具体的な使い方は以下の通り。
- ApplicationPolicyを定義
- ApplicationControllerでPunditを読み込む
- ApplicationPolicyを継承したPolicyクラスを作成
- Controllerでauthorize(policy)を使用、権限によってCRUD等の管理をする
- 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の防ぎ方
ビジネスロジックやプレゼンテーションロジックをコントローラに書かないこと。
ActiveModel::AttributeAssignment
rails learning rake task, cron, whenever