[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する

## 環境
Rails: 5.2.3

## 実装条件
・ユーザー新規登録画面を実装したい
　=>ここではusersとbooksテーブルで実装
・ウィザードフォーム形式で実装する
　（入力画面を複数画面用意し、画面遷移させる。）
　（データベースへの登録は最後に１回のみ行う。）
　=> sessionでデータ保持する方法を使用
・入力フォームで入力した値は複数のデータベースに保存する。
　=> fields_forを使用
・`gem 'devise'`は使用しない
　=> 理由は下記記事の補足参照
　　[[Rails]新規登録画面のウィザードフォーム実装(sessionでデータ保持する）](https://qiita.com/NT90957869/items/56ca4101d7ba37778076)
　=> パスワードは`gem 'bcrypt'`で暗号化する

## パスワードを暗号化できるようにする

##### ▼gem 'bcrypt' をインストール
```ruby:Gemfile
gem 'bcrypt'
```
上記を追記して`bundle install`。

##### ▼usersテーブルに必要なカラムを作成
```ruby:マイグレーションファイル
t.string :password_digest
```
ここに暗号化したパスワードが保存される。

##### ▼モデルに記述する
```ruby:models/user.rb
has_secure_password
```
この記述によりパスワードが暗号化されて`password_digest`カラムに保存される。

## ウィザードフォーム形式で実装する
##### ▼遷移するページ数分、ビューファイルを用意する
ここでは下記２ファイルを用意
step1.html.erb
step2.html.erb

##### ▼ルーティングを設定する
```ruby:config/routes.rb
resources :users do
    collection do
      get 'step1'
      get 'step2'
    end
  end
```
表示するページの分、ルーティングを記述する。

##### ▼ビューファイルを記述

```erb:users/step1.html.erb
<%= form_for @user, url: step2_users_path, method: :get do |f| %>
  email
  <%= f.email_field :email %>
  password
  <%= f.password_field :password %>
  password_confirmation
  <%= f.password_field :password_confirmation %>

  <%= f.submit 'Continue' %>
<% end %>
```
`url: step2_users_path, method: :get`
で２ページ目のルーティングを指定することにより、
submitした際に２ページ目に遷移する。
　※form_withだとなぜか遷移しないので注意

```erb:users/step1.html.erb
  <%= f.password_field :password %>
  <%= f.password_field :password_confirmation %>
```
この部分、パスワードのカラムは「`password_digest`カラム」１つしか作成していないのに入力フォームが２つある。
`has_secure_password`の機能により、ビューに上記の記述をしても、
ちゃんとパスワード２つが検証され暗号化されて「`password_digest`カラム」に保存される。

```erb:users/step2.html.erb
<%= form_for @user, url: users_path do |f| %>
  name
  <%= f.text_field :name %>
  <%= f.submit 'Continue' %>
<% end %>
```
`url: users_path`
でcreateアクションのルーティングを指定することにより、
submitした際にcreateアクションが動く。

##### ▼コントローラーを記述
```ruby:users_controller.rb
  def step1
    @user = User.new # 新規インスタンス作成
  end

  def step2
    # step1で入力した値をsessionに保存
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    @user = User.new # 新規インスタンス作成
  end

  def create
    @user = User.new(
      email: session[:email], # sessionに保存された値をインスタンスに渡す
      password: session[:password],
      password_confirmation: session[:password_confirmation],
      name: user_params[:name]  # step2で入力した値をインスタンスに渡す
    )
    if @user.save
      session[:user_id] = @user.id # ログイン状態維持のためuser_idをsessionに保存
      redirect_to new_user_path
    else
      render '/users/step1'
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
```
これでウィザードフォームについては実装完了。
遷移するページを増やしたければform_forで指定するルーティングを次ページのルーティングにするか、createアクションのルーティングにするか切り替えればOK。

formで入力したデータについてはparamsで取得したデータをsessionに保存し、
最後にcreateアクションでsessionに保存した値をインスタンスに渡す。
ポイントは入力したページの次のページのアクションでsessionに保存の記述をすること。

## 複数のデータベースに保存する実装（fields_forを使用）

##### ▼親モデルと子モデルを用意する
子モデルには親モデルと関連付いたidカラムを作成しておくこと。

```ruby:マイグレーションファイル
t.references :users, foreign_key: true
```

##### ▼アソシエーションを設定する
ここでは仮に user （親モデル）、book （子モデル）が`１対１（has_one）`の関係とする。
それぞれにアソシエーションを記述。
　※`１対多（has_many）`の場合は記述方法が少し違うので注意。

```ruby
# models/user.rb
has_one :book
accepts_nested_attributes_for :book

# models/book.rb
belongs_to :user, optional: true
```

##### ▼ビューファイルに`fields_for`のフォームを追加
```erb:step2.html.erb
<%= form_for @user, url: users_path do |f| %>
  <%= hidden_field_tag :current_step, 'step2' %>
  name
  <%= f.text_field :name %>
  <%= f.fields_for :book do |b| %> // :bookで保存先のモデルを指定
    title
    <%= b.text_field :title %> // fields_forのフォームなので、「b.」で始めている
  <% end %>
  <%= f.submit 'Continue' %>
<% end %>
```

##### ▼ストロングパラメーターに記述を追加
```ruby:users_controller.rb
private

  def user_params
    params.require(:user).permit(
      :email,
      :password,
      :password_confirmation,
      :name,
      book_attributes: [:id, :title]
    )
  end
```
子モデルのparamsは、以下の形で記述する。
`モデル名_attributes: [:id, :カラム名]`
:idを入れているのは、親モデルとの関連付けの「user_idカラム」に値が保存されるようにしている。

こう記述しておくことで、User.saveされた時に自動でbooksテーブルにも入力値が保存される。
また、対応する親モデルのidが自動でuser_idカラムに保存される。

##### ▼コントローラーに記述を追加
```ruby:users_controller.rb
  def step1
    @user = User.new
  end

  def step2
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    @user = User.new
    @user.build_book # bookモデルと関連付ける
  end
```
fields_forを記述したビューを呼び出すアクションに以下を記述する。
`@user.build_モデル名`
ポイントは、**「fields_forを記述したビューを呼び出すアクションに」記述する**、というところ。

##### ▼コントローラーに記述を追加
```ruby:users_controller.rb
def create
    @user = User.new(
      email: session[:email],
      password: session[:password],
      password_confirmation: session[:password_confirmation],
      name: user_params[:name]
    )
    @user.build_book(user_params[:book_attributes]) # 入力値を引数で渡す
    if @user.save
      session[:user_id] = @user.id
      redirect_to new_user_path
    else
      render '/users/step1'
    end
  end
```
これにより@user.save後に自動で、User.createとBook.createが走り、
親モデルと子モデル両方のテーブルに一度に保存される。
子モデルのテーブルには対応した親モデルのidもちゃんと保存される。

ここではuser （親モデル）、book （子モデル）が`１対１（has_one）`の関係の場合の実装を書いているが、`１対多（has_many）`の場合は記述方法が少し違うので注意。
特にところどころ出てくるモデル名が単数形か複数形か変わるので、書き間違いに注意。

## ページ遷移時にバリデーションがかかるようにする
上記のコードのままだと、１ページ目の入力が終わって次のページに遷移する際、バリデーションがかかっていないので、**入力に不備があってもそのまま次のページに遷移できてしまう。**
そのため、入力時、またはページ遷移時にバリデーションをかけ、入力に不備がある場合は元のページに戻りエラーメッセージを表示する必要がある。

ここでは、ページ遷移する際にバリデーションをかける方法を書く。

##### ▼バリデーションをかけるためのメソッドを作成
```ruby:users_controller.rb
# 2step目のページが開く前にメソッドを動かす
before_action :save_to_session, only: :step2

  def step2
    @user = User.new
    @user.build_book
  end

  # sessionに保存しつつ、バリデーションをかけるメソッドを作成
  def save_to_session
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    # バリデーションをかけるため、仮でインスタンスに入力値を入れる
    @user = User.new(
      email: session[:email],
      password: session[:password],
      password_confirmation: session[:password_confirmation],
      name: user_params[:name]
    )
    # インスタンスにバリデーションをかけ、通らなければ1step目のページを再度表示する
    render '/users/step1' unless @user.valid?
  end
```
上記のようにしてみた。
sessionへの保存とバリデーションをかける用のメソッドを新規作成する。
バリデーションは基本はDBに保存する際にかかるため、session保存時にかけることはできない。
そこで、session保存とは別で仮でインスタンスを作成、そこに入力値を入れる。
インスタンスに対しては`.valid?`メソッドでバリデーションをかけることができるので、通らない場合は1step目のページを再度表示させる。

1step目のページを再表示させるために`render`を使用。
`redirect_to`だと再度アクションを動かすため、ページが更新されてしまい、入力値が消えてしまうため。
ページを戻った際に入力値が消えてしまうとまた入力し直しになるのでユーザーに手間がかるのでできれば残したい。
renderだとアクションを動かさずに直接ページをレンダリングするので、更新がかからずに入力値が残る。

また、これを2step目のページが開く前に実行する必要があるため、`before_action`にて2step目のアクションが動く前に実行されるようにする。

これにてページ遷移時にDB保存せずともバリデーションをかけることができる。
しかしエラーメッセージの実装方法によっては、JavaScriptでバリデーションをかける方法もあるかと思う。
あくまで一例ということで。




## あとがき
今回この実装をするに当たって、検索して調べた内容だけでは実装できなかったので、実装方法をここにメモしておく。
苦戦したのは、**「ウィザードフォーム実装」**と、**「fields_forを使った複数テーブルへの保存」**、それぞれの機能については実装方法がいくらでも紹介されていたが、**この２つを組み合わせた実装**というのは調べても出てこなかった。
コントローラー周りの記述が変わってくるので、なかなか思ったように動かずにかなり苦戦した。
記述を少し変えては試しに動かしてbinding.pryでparamsの中身確認して・・・というのを幾度となく繰り返し、ようやく思った通りに動く実装ができた。
少しでも記述が違ったりするとちゃんとデータが取れなかったり保存されなかったりするので注意。

## 参考
[【Rails】fields_for と accepts_nested_attributes_for](http://kzy52.com/entry/2013/07/10/200144)
[Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for)](https://qiita.com/shizuma/items/6f56ca442111ece021b5)
[fields_forにハマった](https://qiita.com/np_misaki/items/e2951572a6a82f87f074)
[Railsで関連モデルを同時に更新したい(has_oneで関連させていたモデルをbuild_associationで安直に生成しようとしたら痛い目をみた話)](https://kimuraysp.hatenablog.com/entry/2016/02/07/120121)