【Rails】Formオブジェクトパターンでの実装
複数のモデルのデータ定義の生成・更新で用いるFormオブジェクトパターン。 デザインパターン(実現したいゴールに対して最適な方法や手順をまとめたもの)の一つです。 アプリの商品購入機能において、こちらのパターンを使用して実装を行ったので、ここに備忘録を残しておきます。 ここの箇所、エラーがたくさん出て試行錯誤を繰り返した内容でもあります。 実装手順とともに、遭遇したエラーも記載して、内容の理解をさらに深めようと思います。
なぜFormオブジェクトパターンを使うのか?
今まで通り、一つのコントローラーの中にそれぞれのモデルに対してparams
を定義し、それをcreate
していけばいいのではないか。
そうすると、もし入力を間違った場合(バリデーションに引っかかった場合)に保存ができません。
実装の手順
①modelsディレクトリ直下にFormオブジェクト用のファイルを生成する
②生成したファイル内に、以下の記述を行う
class Order include ActiveModel::Model //①~ attr_accessor :post_code, :prefecture_id, :city, :address, :building, :phone_number, :user_id, :item_id,:token //~① with_options presence: true do //②~ validates :post_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "例)123-4567"} validates :prefecture_id, numericality: {other_than: 0, message: "can't be blank"} validates :city validates :address validates :phone_number, format: {with: /\d{10,11}/}, length: {maximum: 11} validates :token validates :user_id validates :item_id end //~② def save //③~ order_item = OrderItem.create(user_id:user_id, item_id:item_id) Deliver.create(post_code:post_code, prefecture_id:prefecture_id, city:city, address:address,building:building,phone_number:phone_number, order_item_id:order_item.id) end //~③ end
include ActiveModel::Model
を用いると、form_withやrenderを使えるようになったり、バリデーションの設定ができるようになります。
attr_accessor
メソッドでゲッターとセッターを定義することで、インスタンスを生成した際にform_withの引数として利用することができる。
def index @order = Order.new end def create @order = Order.new(order_params) if @order.valid? pay_item @order.save return redirect_to root_path else render 'index' end end ~~省略~~ def order_params params.require(:order).permit(:post_code, :prefecture_id, :city, :address, :building, :phone_number).merge(user_id: current_user.id, item_id: params[:item_id], token:params[:token]) end
current_userメソッドはコントローラー内に記述すること。(理由は後述) ④form_withメソッドを用いてインスタンスを渡せるような記述を行う
<%= form_with model: @order, url: item_delivers_path,id: 'charge-form', class: 'transaction-form-wrap',local: true do |f| %> <%= render 'shared/error_messages', model: f.object %> ~~後略~~
以下、ビューファイル記述の際の注意点です。
実装中に遭遇したエラーを紐解く
今回は、住所やクレジットカード情報を保存するdeliverモデルと、商品情報とdeliver情報を紐づけるorder_itemモデルのふたつをFormオブジェクトを用いて保存していきました。
①paramsについて、MissingParamsやNoMethodErrorになる
order_itemのparamsがないと怒られたり、アソシエーションが定義されていないと怒られました。
・原因・ 本来、紐付け用に生成したorder_itemモデルをFormオブジェクトとして使用し、そのなかにバリデーションとアソシエーションを定義していました。
class Order include ActiveModel::Model attr_accessor :post_code, :prefecture_id, :city, :address, :building, :phone_number, :user_id, :item_id,:token with_options presence: true do validates :post_code, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "例)123-4567"} validates :prefecture_id, numericality: {other_than: 0, message: "can't be blank"} validates :city validates :address validates :phone_number, format: {with: /\d{10,11}/}, length: {maximum: 11} validates :token validates :user_id validates :item_id end def save order_item = OrderItem.create(user_id:user_id, item_id:item_id) Deliver.create(post_code:post_code, prefecture_id:prefecture_id, city:city, address:address,building:building,phone_number:phone_number, order_item_id:order_item.id) end belongs_to :user belongs_to :item has_one :deliver extend ActiveHash::Associations::ActiveRecordExtensions belongs_to :prefecture end
今まで定義していたモデルは、すべてApplicationRecordが定義されていたため、バリデーションやアソシエーションが組めていました。
Formオブジェクトは手入力で生成するので、アソシエーションやsaveメソッド、current_userメソッドは使えません。
(バリデーションは、include ActiveModel::Model
を定義しているので使用可能。)
・解決策・
Formオブジェクト内にはバリデーションのみ記述すること。
②値の取得・保存に関するエラー
購入するitemのデータを取得したいのですが、find
メソッドを使っても見つけることができません。
・原因・
params
での値の取得の仕方
def item_find @item = Item.find(params[:id]) end
このままでは、orderモデルのparams
のIDを取得してしまいます。
欲しいのはitemのID。
create
するときの()内の記述
def save order_item = OrderItem.create(user_id:user.id, item_id:item.id) Deliver.create(post_code:post_code, prefecture_id:prefecture_id, city:city, address:address,building:building,phone_number:phone_number, order_item_id:order_item.id) end
ここの部分、ちゃんと理解していなかったのでなんとなくで書いていました。反省。 ・解決策・
params
の記述を以下に変更。
def item_find @item = Item.find(params[:item_id]) end
params
内の[]内は、params
のどこの値を取得するかです。
今回は、紐づいているitemのIDを取得したかったため、params
のmerge
部分に記述されているカラム名を選択しています。
def save order_item = OrderItem.create(user_id:user_id, item_id:item_id) Deliver.create(post_code:post_code, prefecture_id:prefecture_id, city:city, address:address,building:building,phone_number:phone_number, order_item_id:order_item.id) end
そもそもの話ですが…
Xxx.create(name: name,..........,user_id:user_id)
このような記述があった場合、:の左側はテーブルのカラム名、右側はストロングパラメーターやattr_accecorメソッド、ビューなどに入力された値をそれぞれ指します。
ここはなんとなくの私の認識なので間違っていたら訂正をお願いしたいのですが、
user_id→カラム名とかform_with内とかで使用する名前で、値を入れる箱
user.id→値そのもの。これをカラムとかの箱に入れる。
このように認識しています。
今回は、ビューファイル内等すべてuser_id
で記述していたので、create
()内の記述もこれに統一しました。
まとめ
Formオブジェクトを作成していく上では、モデル同士やモデルとコントローラー、ビューなどそれぞれの関係性をきちんと理解していないと記述できないな、と感じました。(だからエラー沼に落ちた…) 流れの把握もそうなのですが、それぞれが値なのか、カラム名なのかという細かいところまで確認しながら実装していかないといけませんね。 最初は時間がかかるかもしれませんが、徐々に慣れていきたいと思います。