【ポイント:擬似モデル】Formオブジェクトパターンを理解する
Railsのデザインパターンの一つであるFormオブジェクトパターンを使って、フォームから一度の送信で複数のテーブルに安全に情報を送る方法について学びました。
フォームから複数モデルの情報を送る際の問題点
form_withメソッドに渡しているのは単一のモデルのインスタンスなので、他のモデルクラスがもつ属性の情報が送れません。
Formオブジェクトパターンを使わない場合の解決法
form_with
で:model
の指定をやめ、params
に送信した全ての値が入るようにします。
before
<%= form_with(model: @user, url: donations_path, local: true) do |form| %>
after
<%= form_with(url: donations_path, local: true) do |form| %>
!注意!
:model
に指定していたモデルのバリデーションが活用できなくなる。(後ほど解消します)
それぞれのテーブルに必要な値をparamsから取り出す
以下の例では、3つの別々のprivateメソッドを定義し、paramsからusers
とaddresses
とdonations
のそれぞれのテーブルに必要な値のみを取り出して使うことができるようにしています。
class DonationsController < ApplicationController def index end def new @user = User.new end def create user = User.create(user_params) Address.create(address_params(user)) Donation.create(donation_params(user)) redirect_to action: :index end private def user_params params.permit(:name, :name_reading, :nickname) end def address_params(user) params.permit(:postal_code, :prefecture, :city, :house_number, :building_name).merge(user_id: user.id) end def donation_params(user) params.permit(:price).merge(user_id: user.id) end end
Formオブジェクトパターンを使う場合の解決法
それでは、Formオブジェクトを使って、上の方法でモデルのバリデーションが使えない問題を解消していきます。
Formオブジェクトパターン
大体以下のような流れで実装していきます。
- フォームから送られる全てのパラメータを属性として持つような擬似モデルクラスを
models/
直下に作成 - それぞれのカラムに対するバリデーションをここに書く(本物のモデルクラスではなく、のほうでバリデーションする)
- それぞれのテーブルにデータを保存する処理を書く(
save
メソッドなど) - フォームから直接アクセスされる方のコントローラの
new
とcreate
で、擬似モデルクラスのインスタンスを作成するようにする - コントローラのストロングパラメータも、擬似モデルクラスが必要とする属性値を渡すようにする
要するに何をするかというと、複数のモデルの情報をあたかも単一モデルの情報であるかのように扱える擬似モデルを使うということです。
ActiveModel::Model
クラスにincludeすることで、AcriveRecordを継承したクラスのインスタンス同様にform_withやrenderなどのヘルパーメソッドの引数になれたり、バリデーションが使えたりします。今回作成するFormオブジェクト(=擬似モデルクラス)も、ActiveModel::Modelをincludeすることで本物のモデル同様にバリデーションなどが使えるようになります。
class UserDonation include ActiveModel::Model attr_accessor :name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price # ここにバリデーションの処理を書く def save # 各テーブルにデータを保存する処理を書く end end
例えば、user
モデル、address
モデル、donation
モデルの3つのモデルに対する擬似モデルは以下のような感じです。
app/models/user_donation.rb
class UserDonation include ActiveModel::Model attr_accessor :name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price with_options presence: true do #userモデルに関するvalidation validates :name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: "is invalid. Input full-width characters." } validates :name_reading, format: { with: /\A[ァ-ヶー-]+\z/, message: "is invalid. Input full-width katakana characters." } validates :nickname, format: { with: /\A[a-z0-9]+\z/i, message: "is invalid. Input half-width characters." } #addressモデルに関するvalidation validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)" } end #addressモデルのprefectureに関するvalidation validates :prefecture, numericality: { other_than: 0, message: "can't be blank" } #donationモデルに関するvalidation validates :price, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 1000000, message: "is out of setting range" } def save user = User.create(name: name, name_reading: name_reading, nickname: nickname) Address.create(postal_code: postal_code, prefecture: prefecture, city: city, house_number: house_number, building_name: building_name, user_id: user.id) Donation.create(price: price, user_id: user.id) end end
次に、コントローラ側から擬似モデルのインスタンスを作成し、擬似モデルのバリデーションの実行やsave
メソッドの呼び出しを行います。
app/controllers/donations_controller.rb
class DonationsController < ApplicationController def index end def new @user_donation = UserDonation.new end def create @user_donation = UserDonation.new(donation_params) if @user_donation.valid? @user_donation.save redirect_to action: :index else render action: :new end end private def donation_params params.require(:user_donation).permit(:name, :name_reading, :nickname, :postal_code, :prefecture, :city, :house_number, :building_name, :price) end end
あとは、newアクションのビューでform_withの:model
属性に擬似モデルのインスタンスを指定してあげます。
app/views/donations/new.html.erb
<%= form_with(model: @user_donation, url: donations_path, local: true) do |form| %> <%= render 'error_messages', model: @user_donation %> #省略
これで、3つのモデルをまるで1つのモデルであるかのように扱えるようになりました。 あとは、Rspec等で
Formオブジェクトパターンの単体テストコード
Rspec、FactoryBot、Fakerのテストコード3種の神器(?)を使ってテストコードを記述していきます。(テストコードは省略) FactoryBotとFakerについての備忘録がありますので、そちらもご活用ください。
まずはRspec導入
Gemfile
group :development, :test do # 中略 gem 'rspec-rails' gem 'factory_bot_rails' gem 'faker' end
bundle install rails g rspec:install
.rspecファイルに追記してテスト実行ログのフォーマットを整える。
--require spec_helper --format document
擬似モデルuser_donation
のテストコードファイル作成
rails g rspec:model user_donation
作成されたuser_donation_spec.rb
ファイルの中で、全てのバリデーションをテストするように書けば単体テスト完了です。