Share
Embed
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
ShinkuFencer
October 23, 2021
Transcript
37 slides
-
今回サンプルとして扱うアプリケーション 4 $ bin/rails g scaffold post owner_id:bigint title homepage_url:text
message:text • 今回は「投稿」のアプリケーションなので “Post” というモデルを中心に scaffoldをつかって作ったものを基点に話を進めていきます。 -
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 -
scaffoldで生成されるコードについてのおさらい • scaffoldで指定したモデル(今回はPost)を操作するためのすべてのアク ションを自動で生成します • 単一のモデルを取り扱うアクション(showやedit)ではURLで指定したid の情報をインスタンス変数で取り扱います ◦ 「パラメータを元に対象のモデルをインスタンス変数に格納する」という一連の作業を、 set_***という形で作成されます
◦ set_***は各アクションで使えるようにbefore_actionで呼び出される形になっています 6 -
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 -
「依存する呼び出し」の例 • 例えば「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を模して作ると次のようになる -
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 -
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が呼び出されて いることが前提の記述になり、依存している状態 になるので、変更に弱くなる -
「実行順序管理の煩雑化」の例 • Scaffoldの形を意識すると、インスタンス変数の分だけbefore_actionで メソッドを書く形になってしまうため、メソッドの数が増える • なるべく対象のアクションごとにメソッドはまとめたいが「依存する呼び 出し」がある場合に実行されるアクションの指定が同じ場合でも書き分け てあげる必要が出てきてしまう 15 •
例えば以下のような要件が追加されると考える • 詳細表示画面(show) に「関連投稿」「おすすめの投稿」を表示したい ◦ 「関連投稿」は表示している投稿から導出できる ◦ 「おすすめ投稿」は「ピックアップ投稿があれば、それを除いた新着投稿」 • 「ピックアップ投稿」は元の投稿と新着投稿から決定される投稿 • 「おすすめ投稿」はeditでも表示する -
前述の仕様を実装したソースコード 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 -
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の処理を入れる必要があるためできない -
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はインスタンス変数を必要としな いのでローカル変数に書き換えることができた -
「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 -
新規作成ページと編集ページはログインしていないと見れない 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 -
横断的関心事とは • 大きなプログラムの中で、それぞれの処理において「その処理が関心のあ ることがら(内容)」に関して示す言葉として関心事と表現する。 • メソッドなどを実装するときにそのメソッドが主要として考えるべき内容 のことを本質的関心事(core concern) , それに対して多くの部分で使われ
るような共通的な処理のことを横断的関心事(cross-cutting concern)と いう 29 関心事というフレーズにしては以下のページにまとめてます 「関心の分離」をするメリットを料理レシピを通して考える - コード日進月歩 https://shinkufencer.hateblo.jp/entry/2021/09/18/233033 -
特殊計算処理メソッド 横断的関心事とは 30 処理開始時間Logging 計算処理 処理終了時間Logging 横断的関心事の例としてLoggingがよくあげられる 「計算処理」から見たときにLogging処理自体は 本当に行いたい処理かといわれるとそうではなく また、様々なところで共通して利用できるので
Logging処理は”横断的な関心”となる。 こちらはBEAR.Sundayの説明スライドを参考にさせていただきました Slide | BEAR.Sunday https://bearsunday.github.io/slide.html -
今回の例を踏まえた横断的関心事 例えば「投稿の詳細表示ページ」で処理させたいことは… 31 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること 選択した投稿をふまえ
たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト -
横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 32 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること
選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 編集ページでは「投稿の閲覧」にまつわることがメインなのでそれらを本質的 な関心として捉えると以下のように分けることができる -
横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 33 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること
選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト ログインのチェック自体は編集する際には必須の処理であるがアプリケーショ ン全体で横断して関心のある処理なので横断的な関心として捉える -
横断的関心事 本質的関心事 今回の例を踏まえた横断的関心事 34 選択した投稿の詳細が 閲覧できること 新着投稿一覧が見れる こと 選択した投稿の関連投 稿一覧が見れること
選択した投稿をふまえ たおすすめの投稿一覧 が見れること ログインしてなければ indexにリダイレクト 関連投稿に関しては投稿詳細(show)でも使える共通処理ではあるが、他の場所 に使うような横断的なものか?と考えると趣が異なるため、別と考える -
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