Save 37% off PRO during our Black Friday Sale! »

before_actionとのつらくならない付き合い方 #kaigionrails / how to using "before_action" with happy in Rails

Be85784057d8cad4fbb2dd43cbdecf89?s=47 ShinkuFencer
October 23, 2021
1.3k

before_actionとのつらくならない付き合い方 #kaigionrails / how to using "before_action" with happy in Rails

Kaigi on Rails 2021 Day2 で発表する内容です。

【資料内参考したものリンク】

パーフェクトRuby on Rails【増補改訂版】
https://gihyo.jp/book/2020/978-4-297-11462-6

「関心の分離」をするメリットを料理レシピを通して考える - コード日進月歩 https://shinkufencer.hateblo.jp/entry/2021/09/18/233033

Slide | BEAR.Sunday
https://bearsunday.github.io/slide.html

Rails の before_action :set_* って必要? - ネットの海の片隅で https://osa.hatenablog.com/entry/good-bye-before-action-setter

Controllerのbefore_actionにおける インスタンス変数セットについて https://www.slideshare.net/pospome/controllerbeforeaction

Be85784057d8cad4fbb2dd43cbdecf89?s=128

ShinkuFencer

October 23, 2021
Tweet

Transcript

  1. before_actionとの つらくならない付き合い方 Kaigi on Rails 2021 2021/10/23 しんくう@shinkufencer

  2. 本日しゃべること 2 • 大きく2パートでしゃべります • Part1は「before_actionで変数セットはつらいよ」 • Part2は「before_actionがマッチする処理の見極め術」 • Part1と2ともサンプルアプリケーションのコード使って説明します

  3. 今回サンプルとして扱うアプリケーション • CGI風ゲストブック式掲示板 • ひとこと投稿ができるアプリケーショ ン投稿一覧確認、投稿の閲覧、編集が できる • GitHubでログインすることで ユーザー(User)が作成され

    投稿(Post)ができ、編集や閲覧がお こなえる 3
  4. 今回サンプルとして扱うアプリケーション 4 $ bin/rails g scaffold post owner_id:bigint title homepage_url:text

    message:text • 今回は「投稿」のアプリケーションなので “Post” というモデルを中心に scaffoldをつかって作ったものを基点に話を進めていきます。
  5. scaffoldで作られたコード 5 class PostsController < ApplicationController before_action :set_post, only: %i[

    show edit update destroy ] def index @posts = Post.all end def show end def new @post = Post.new end def edit end def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: "Post was successfully created." } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: "Post was successfully updated." } format.json { render :show, status: :ok, location: @post } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def destroy @post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def set_post @post = Post.find(params[:id]) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end
  6. scaffoldで生成されるコードについてのおさらい • scaffoldで指定したモデル(今回はPost)を操作するためのすべてのアク ションを自動で生成します • 単一のモデルを取り扱うアクション(showやedit)ではURLで指定したid の情報をインスタンス変数で取り扱います ◦ 「パラメータを元に対象のモデルをインスタンス変数に格納する」という一連の作業を、 set_***という形で作成されます

    ◦ set_***は各アクションで使えるようにbefore_actionで呼び出される形になっています 6
  7. before_actionで 変数セットはつらいよ 7 Part 1

  8. scaffoldで作られたコードにおけるbefore_actionの実行部分 8 class PostsController < ApplicationController before_action :set_post, only: %i[

    show edit update destroy ] def index @posts = Post.all end def show end def new @post = Post.new end def edit end def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: "Post was successfully created." } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: "Post was successfully updated." } format.json { render :show, status: :ok, location: @post } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end def destroy @post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def set_post @post = Post.find(params[:id]) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end before_action :set_post, only: %i[ show edit update destroy ] def set_post @post = Post.find(params[:id]) end
  9. before_actionとインスタンス変数 • おさらいで話をしたとおりscaffoldではbefore_actionは各アクションで 共通に使われるインスタンス変数のセットをするためのものとして自動生 成されます • このやりかたにならい「インスタンス変数のセットをbefore_actionで行 う」というやり方を行うコードを多々見かける場面がある • しかしこの方法を続けるとコードが成長していくにつれてつらくなるポイ

    ントがいくつかある 9
  10. つらくなるポイント① 依存する呼び出し 10

  11. 「依存する呼び出し」の例 • 例えば「showとeditに新しく追加された投稿を最新から3件(ただし表示 している投稿以外)を出したい」というような仕様が発生したとします • 「指定した投稿以外を取得できる」をModelに以下のように作る 11 # @param [Post]

    exclude_post 除外したいPost # @return [ActiveRecord::Relation<Post>] 引数指定を除いたPost一覧 def self.new_arrival_posts(exclude_post) self.where.not(id: exclude_post.id).order(created_at: :desc) end • ViewでつかえるようにControllerでインスタンス変数にセットする必要 となる、その場合scaffoldを模して作ると次のようになる
  12. Modelで取得した値をbefore_actionでsetする 12 class PostsController < ApplicationController before_action :set_post, only: %i[

    show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] ################# # 中略 # ################ private def set_post @post = Post.find(params[:id]) end def set_new_arrival_posts @new_arrival_posts = Post.new_arrival_posts(@post) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end
  13. Modelで取得した値をbefore_actionでsetする 13 class PostsController < ApplicationController before_action :set_post, only: %i[

    show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] ################# # 中略 # ################ private def set_post @post = Post.find(params[:id]) end def set_new_arrival_posts @new_arrival_posts = Post.new_arrival_posts(@post) end def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end set_new_arrival_postsは@postが呼び出されて いることが前提の記述になり、依存している状態 になるので、変更に弱くなる
  14. つらくなるポイント② 実行順序管理の煩雑化 14

  15. 「実行順序管理の煩雑化」の例 • Scaffoldの形を意識すると、インスタンス変数の分だけbefore_actionで メソッドを書く形になってしまうため、メソッドの数が増える • なるべく対象のアクションごとにメソッドはまとめたいが「依存する呼び 出し」がある場合に実行されるアクションの指定が同じ場合でも書き分け てあげる必要が出てきてしまう 15 •

    例えば以下のような要件が追加されると考える • 詳細表示画面(show) に「関連投稿」「おすすめの投稿」を表示したい ◦ 「関連投稿」は表示している投稿から導出できる ◦ 「おすすめ投稿」は「ピックアップ投稿があれば、それを除いた新着投稿」 • 「ピックアップ投稿」は元の投稿と新着投稿から決定される投稿 • 「おすすめ投稿」はeditでも表示する
  16. 前述の仕様を実装したソースコード 16 class PostsController < ApplicationController before_action :set_post, only: %i[

    show edit update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] before_action :set_relation_posts,:set_pickup_post, only: %i[ show ] before_action :set_recommend_posts, only: %i[ show edit] ################# # 中略 # ################ # @return [ActiveRecord::Relation<Post>] 閲覧中の投稿に関連する投稿 def set_relation_posts @relation_posts = Post.relation_posts(@post) end # @return [Post] ピックアップしたい投稿、対象の投稿と新着投稿から専用のメソッドで出す def set_pickup_post @pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) end # @return [ActiveRecord::Relation<Post>] 状況を加味したおすすめ投稿一覧 def set_recommend_posts # おすすめはピックアップを除外した新着ポスト @recommend_posts = Post.new_arrival_posts(@pickup_post) end
  17. class PostsController < ApplicationController before_action :set_post, only: %i[ show edit

    update destroy ] before_action :set_new_arrival_posts, only: %i[ show edit ] before_action :set_relation_posts,:set_pickup_post, only: %i[ show ] before_action :set_recommend_posts, only: %i[ show edit] ################# # 中略 # ################ # @return [ActiveRecord::Relation<Post>] 閲覧中の投稿に関連する投稿 def set_relation_posts @relation_posts = Post.relation_posts(@post) end # @return [Post] ピックアップしたい投稿、対象の投稿と新着投稿から専用のメソッドで出す def set_pickup_post @pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) end # @return [ActiveRecord::Relation<Post>] 状況を加味したおすすめ投稿一覧 def set_recommend_posts # おすすめはピックアップを除外した新着ポスト @recommend_posts = Post.new_arrival_posts(@pickup_post) end 順番の制約で複雑になるところ 17 最初のshowとeditのbefore_actionとして set_recommend_postsの処理も記載したかったが set_pickup_postの処理を入れる必要があるためできない
  18. 【Question】 before_actionで インスタンス変数のセットを つらくならないようにするには どうするのがいいの? 18

  19. 【Answer】 before_actionでやらない 19

  20. インスタンス変数のセットはbefore_actionでやらない • scaffoldは文字通り「足場」、足場はそのまま使うものではなく、その足 場を元に作り上げるものなので踏襲してつくらない • アプリケーションが成長していくにつれて、各actionごとにインスタンス 変数の意味や使われ方はそれぞれ異なってくる可能性がある • また「依存する呼び出し」のような前提として必要なインスタンス変数が ある場合、actionの中の記述で書いたほうがわかりやすい

    20
  21. class PostsController < ApplicationController def index @posts = Post.all end

    def show @post = Post.find(params[:id]) @new_arrival_posts = Post.new_arrival_posts(@post) @relation_posts = Post.relation_posts(@post) pickup_post = Post.pickup_post(current_post: @post, new_arrival_posts: @new_arrival_posts) @recommend_posts = Post.new_arrival_posts(pickup_post) end def new @post = Post.new end def edit @post = Post.find(params[:id]) @new_arrival_posts = Post.new_arrival_posts(@post) @relation_posts = Post.relation_posts(@post) @recommend_posts = Post.new_arrival_posts(nil) end ### 中略 ### def destroy post = Post.find(params[:id]) post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: "Post was successfully destroyed." } format.json { head :no_content } end end private def post_params params.require(:post).permit(:owner_id, :title, :homepage_url, :message) end end before_actionをやめたコード 21 行数としては増えたが、メソッド内で変数のセット まで完結しているので見やすい また、pickup_postはインスタンス変数である必要 がないのでスコープを狭くすることができる destroyのactionはインスタンス変数を必要としな いのでローカル変数に書き換えることができた
  22. インスタンス変数セットが ダメなのはわかった ではbefore_actionは どのような場面で使うとよいのか? 22

  23. before_actionが マッチする処理の見極め術 23 Part 2

  24. 「before系」フィルタのよくある使われ方として、ユーザーがアクションを実行する前にログイ ンを要求するというのがあります。このフィルタメソッドは以下のような感じになるでしょう。 before_actionはどういう処理とマッチするのか • before_actionが合う処理の具体例はRailsガイドにすでに書いてある 24 “ ” class ApplicationController

    < ActionController::Base before_action :require_login private def require_login unless logged_in? flash[:error] = "You must be logged in to access this section" redirect_to new_login_url # halts request cycle end end end Action Controller の概要 - Railsガイド より https://railsguides.jp/action_controller_overview.html
  25. before_actionはどういう処理とマッチするのか • Railsガイドの例をもう少し拡張し、「新規作成ページと編集ページはログ インしていないと見れない」とすると以下のような次のようなコードにな る 25

  26. 新規作成ページと編集ページはログインしていないと見れない 26 class ApplicationController < ActionController::Base helper_method :logged_in? helper_method :session_user

    private def session_user @memorize_session_user ||= User.find_by_id(session[:user_id]) end def logged_in? !!session_user end def require_login return if logged_in? redirect_to root_path, notice: "ログインしてください " end end class PostsController < ApplicationController before_action :require_login, only: %i[ new, edit] こちらのコードは「パーフェクトRuby on Rails【増補改訂版】」を元に作成しました https://gihyo.jp/book/2020/978-4-297-11462-6
  27. before_actionを使う使わないの見極め • Railsガイドでもあるように「ログイン状態の事前確認」には適している • 「複数の場所で使うようインスタンス変数のセット」には適していない 27 事例はわかったが「どういう処理で使って良いのか」の判断が分類しにくい そのため今回見極めのひとつの方法として「横断的関心事」を取り上げます

  28. 28 横断的関心事 Crosscutting-Concern

  29. 横断的関心事とは • 大きなプログラムの中で、それぞれの処理において「その処理が関心のあ ることがら(内容)」に関して示す言葉として関心事と表現する。 • メソッドなどを実装するときにそのメソッドが主要として考えるべき内容 のことを本質的関心事(core concern) , それに対して多くの部分で使われ

    るような共通的な処理のことを横断的関心事(cross-cutting concern)と いう 29 関心事というフレーズにしては以下のページにまとめてます 「関心の分離」をするメリットを料理レシピを通して考える - コード日進月歩 https://shinkufencer.hateblo.jp/entry/2021/09/18/233033
  30. 特殊計算処理メソッド 横断的関心事とは 30 処理開始時間Logging 計算処理 処理終了時間Logging 横断的関心事の例としてLoggingがよくあげられる 「計算処理」から見たときにLogging処理自体は 本当に行いたい処理かといわれるとそうではなく また、様々なところで共通して利用できるので

    Logging処理は”横断的な関心”となる。 こちらはBEAR.Sundayの説明スライドを参考にさせていただきました Slide | BEAR.Sunday https://bearsunday.github.io/slide.html
  31. 今回の例を踏まえた横断的関心事 例えば「投稿の詳細表示ページ」で処理させたいことは… 31 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ

    たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト
  32. 横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 32 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること

    選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 編集ページでは「投稿の閲覧」にまつわることがメインなのでそれらを本質的 な関心として捉えると以下のように分けることができる
  33. 横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 33 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること

    選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト ログインのチェック自体は編集する際には必須の処理であるがアプリケーショ ン全体で横断して関心のある処理なので横断的な関心として捉える
  34. 横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 34 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること

    選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 関連投稿に関しては投稿詳細(show)でも使える共通処理ではあるが、他の場所 に使うような横断的なものか?と考えると趣が異なるため、別と考える
  35. 横断的関心事に着目するということ • このように「その処理で横断的な関心事は何か?」という目線を持ち込む ことでbefore_actionに適当な処理を絞り込むことができる • 複数の処理で使う似たような処理が出てくるケースがあるがそれはDRY原 則として「共通化できる処理」である場合が多く、横断的であるかはまた 別になるのでそのような処理の見極めにもこの切り口は役に立つと思われ る 35

  36. まとめ 36 • インスタンス変数をsetするbefore_actionは複雑化するのでやめよう • before_actionを利用すべきかどうかは判断しづらいことも多いので 「横断的関心事」かどうか?で見極めると分けやすいのでオススメ

  37. 37 Thanks! 参考にしたサイトなど: しんくう / shinkufencer   @shinkufencer コード日進月歩 https://shinkufencer.hateblo.jp/

    Rails の before_action :set_* って必要? - ネットの海の片隅で https://osa.hatenablog.com/entry/good-bye-before-action-setter Controllerのbefore_actionにおける インスタンス変数セットについて https://www.slideshare.net/pospome/controllerbeforeaction