Madogiwa Blog

主に技術系の学習メモに使っていきます。

Railsで確認画面を挟んで登録する場合どう実装するのがよさそうか考えてみた

はじめに

Railsで確認画面を実装すると毎回、ベストプラクティス的な解決方法が見つからず悩むので、どういう実装をするのが良いのか少し感がてみました。(私の個人的な見解です🙇‍♂️)

確認画面の難しさ

なぜ確認画面の実装で悩むのか自分で考えてみたところ、確認画面への遷移の際に前の画面の情報を引き継ぐ必要があるが、引き継ぐ方法が中々難しいというのがあるのかなと思います。

確認画面の挟む場合のリクエストは下記のような流れになると思うのですが、HTTPはステートレスなプロトコルなのでリクエスト単位にデータを毎回送信する必要があります。

作成--確認-->投稿
or
作成--確認-->作成

毎回データをリクエストに乗せて情報をやりとりしないといけないので手順が多く、実装及びデータの持ち回りも煩雑になりがちなのかなと思います。

よく見るやり方

確認画面を実装する場合によく目にするのがhiddenタグを使って送信する方法ですね👀 下記のような、よくあるブログを投稿するようなcontrollerがあったとします。

# controller
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def new
    @post = Post.new
  end

  def confirm
    # editとnewに対応させるためfind_or_initializeする
    @post = Post.find_or_initialize_by(id: params[:id])
    @post.assign_attributes(post_params)
  end

  def create
    @post = Post.new(posts_params)
    if @post.save
      redirect_to posts_path
    else
      render :new, @post
    end
  end
end

formは普通なのですが、ポイントとしてはformのpost先をconfirmにして、confirmformでは入力値の表示と、hiddenで入力値を仕込むことによって次の投稿作成のリクエストでもパラメーターに入力画面で設定された値を引き継ぐことができるというものですね。

# form
<%= form_with(model: post, local: true, url: { action: :confirm }) do |form| %>
  <p>タイトル: <%= f.text_field :title %></p>
  <p>本文: <%= f.text_field :content %></p>
  <%= f.submit %>
<% end %>

# confirm
<%= form_with model: post, local: true do |f| %>
  <p>タイトル: <%= post.title %> </p>
  <p>本文: <%= post.content %></p>
  <%= f.hidden :title %>
  <%= f.hidden :content %>
  <%= f.submit %>
<% end %>

このやり方はRailsの機能を使ってシンプルに実装できて非常にわかりやすくて良いやり方だと思うのですが、実際に自分で書いてメンテナンスしていくと下記のような懸念事項が生まれ、他のやり方だと解決できるのかなと思い、今回ちょっと他の方法も考えてみました🤔

  • hiddenで送信する項目が多くなるをformが長くなり煩雑になる
  • hiddenの場合、確認画面上で値を編集され偽装される可能性があるので思わぬ項目が入力値として与えられる可能性がある
  • そもそも入力値を確認画面を表示するという要件でサーバー側にリクエストを送信する必要はあるのか

その他のやり方を検討してみる

セッションに入れる

ショッピングカート等しかり、画面をまたぐ情報を保存する先として先ず思いつくのがセッションだったので、セッションに登録するような実装を考えてみます。

確認画面でパラメーターとして取得した入力値をセッションにいれてあげます。

class PostsController < ApplicationController
  def confirm
    @post = Post.find_or_initialize_by(id: params[:id])
    session[:post] = post_params
    @post.assign_attributes(session[:post])
  end

  def create
    @post = Post.new(posts_params)
    if @post.save
      session.delete(:post)
      redirect_to posts_path
    else
      render :new, @post
    end
  end
end

セッションに入力値が保存されているので、下記のようにconfirmからhidden_tagを使って引き渡す必要がなくなりました。

# confirm
<%= form_with model: post, local: true do |f| %>
  <p>タイトル: <%= post.title %> </p>
  <p>本文: <%= post.content %></p>
  <%= f.submit %>
<% end %>

sessionによる実装は、下記のような感じですかね。

  • hiddenで送信する項目が多くなるをformが長くなり煩雑になる --- △
    • 割とシンプルに実装できるが、セッションの破棄等の管理コストが発生するため一長一短か
  • hiddenの場合、確認画面上で値を編集され偽装される可能性があるので思わぬ項目が入力値として与えられる可能性がある --- ○
    • hiddenで送らないので確認画面で入力値をいじるようなことは行えない
  • そもそも入力値を確認画面を表示するという要件でサーバー側にリクエストを送信する必要はあるのか --- ×
    • リクエストしてセッションに詰めるので、リクエストは必要

これだけみるとセッションのほうがシンプルに実装出来て良い感じがするんですが、セッションを使うと同一ユーザーが同時にリクエストを送信した場合等、セッション破棄のタイミング等によって不正なデータが出来てしまう可能性があるのがネックですね。。。

未公開状態でDBに保存する

これはconfirmに遷移したときにデータを非公開で保存してしまえ!という対応です。先程のセッションの例の保存先をDBにしたというものですね。

modelにpublisedというdefault: falseという項目を用意しておいて、confirmのときに未公開の状態で保存して、実際に確認画面でsubmitしたときに保存していた投稿をfindして公開状態にupdateするという内容です。

class PostsController < ApplicationController
  def confirm
    @post = Post.find_or_initialize_by(id: params[:id])
    @post.assign_attributes(post_params)
    target_render = @post.new_record? ? :new : :edit
    target_render = :confirm if @post.valid?
    @post.save
    render target_render
  end

  def update
    @post.published = true
    if @post.update(post_params)
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end
end

DBに保存しているので下記のようにconfirmからhidden_tagを使って引き渡す必要がなくなりました。

# confirm
<%= form_with model: post, local: true do |f| %>
  <p>タイトル: <%= post.title %> </p>
  <p>本文: <%= post.content %></p>
  <%= f.submit %>
<% end %>

DBに未公開で保存する実装は、下記のような感じですかね。

  • hiddenで送信する項目が多くなるをformが長くなり煩雑になる --- ×
    • hiddenで引き継がなくて良くなった一方、controller側の実装はちょっと煩雑になりそう。
    • submitせずに離脱された場合にゴミデータが残る可能性がある。(仕様として組み込んだ場合には途中離脱しても続きから再開できるという副効用もあるかも)
  • hiddenの場合、確認画面上で値を編集され偽装される可能性があるので思わぬ項目が入力値として与えられる可能性がある --- ○
    • hiddenで送らないので確認画面で入力値をいじるようなことは行えない。
  • そもそも入力値を確認画面を表示するという要件でサーバー側にリクエストを送信する必要はあるのか --- ×
    • ここはリクエストしてDBに詰めるので、リクエストは必要

下書き機能とセットで考えるならありかもしれないですが、確認画面をはさみたいという要件だけでDBに保存するのは不要なDBアクセスも増えますし、実装も複雑になるのでイマイチな感じがしますね。。。 (私の実装が微妙な点も大いにありますが。。。)

JavaScriptでフォームを作る

ここは言わずもがな、なのであれですがJavaScript側でプレビュー機能付きのフォームを作ってあげるというものですね。

昨今のRails環境ではWebpackerが導入されモダンなフロントエンド環境も構築しやすくなっていると思うので普通に選択肢に入ってくるのかなと思います。

  • hiddenで送信する項目が多くなるをformが長くなり煩雑になる --- △
    • hiddenで引き継がなくて良い。
    • CSRF対策やValidation等、form_withを使っていればよしなにやってくれるところを対応しないといけないので一手間かかる。
  • hiddenの場合、確認画面上で値を編集され偽装される可能性があるので思わぬ項目が入力値として与えられる可能性がある --- ○
    • クライアントサイドでの実装ではあるがvalidation等を設定できるためhiddenより堅牢。(サーバーサイドでのチェックは必須)
  • そもそも入力値を確認画面を表示するという要件でサーバー側にリクエストを送信する必要はあるのか --- ○
    • クライアントサイドで行うためリクエストは不要。

リッチな投稿フォームにプレビュー機能が必要であればJavaScript側で実装してあげるのが良さそうですが、Rails側のform_withで十分な投稿フォームまでJavaScriptで書くのはイマイチな感じがしますね。。。

まとめ

色々考えてみて、私なりには一応下記で落ち着いたのですが、まだいいやり方もありそうで奥が深いですね。。。

  • 画面項目が少なく動きの少ない投稿フォームの場合
    • 項目が少なければhidddenタグが多く煩雑になるようなことはなさそうなのでhiddenに詰める。(hiddenタグの書き換えによるリスクは考慮する)
    • 同時実行の考慮が不要及び、modelのバリデーション等で防げない改竄がリスクとしてあればセッションに詰めるやり方も検討する。
  • 画面項目が多く動きの少ない投稿フォームの場合
    • 画面項目が多い場合にはユーザーが離脱した際の再入力コストも高いはずなので、下書き機能とセットでDBに保存方法を検討する。
  • 動きの多いインタラクティブな投稿フォームの場合

参考

qiita.com

eeedotweb.com

hacknote.jp