【Rails】Formオブジェクトパターンでの実装

複数のモデルのデータ定義の生成・更新で用いるFormオブジェクトパターン。
デザインパターン(実現したいゴールに対して最適な方法や手順をまとめたもの)の一つです。
アプリの商品購入機能において、こちらのパターンを使用して実装を行ったので、ここに備忘録を残しておきます。
ここの箇所、エラーがたくさん出て試行錯誤を繰り返した内容でもあります。
実装手順とともに、遭遇したエラーも記載して、内容の理解をさらに深めようと思います。


なぜFormオブジェクトパターンを使うのか?

今まで通り、一つのコントローラーの中にそれぞれのモデルに対してparamsを定義し、それをcreateしていけばいいのではないか。
そうすると、もし入力を間違った場合(バリデーションに引っかかった場合)に保存ができません。

f:id:programmingnuoh:20210327165829j:plain
Formオブジェクトパターンを用いた時の情報の流れ


実装の手順

①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

  • ①form_withメソッドに対応する機能とバリデーションを行う機能を持たせる記述を行う
    include ActiveModel::Modelを用いると、form_withやrenderを使えるようになったり、バリデーションの設定ができるようになります。
    attr_accessorメソッドでゲッターとセッターを定義することで、インスタンスを生成した際にform_withの引数として利用することができる。

  • ②バリデーションの記述
  • ③saveメソッドの定義
    ActiveRecordを継承していないため、saveメソッドを手入力で記述。
    そうすることで、テーブルへのデータ保存が可能になります。

    ③コントローラーにFormオブジェクトのインスタンスを生成する記述を行う

      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 %>
        
    ~~後略~~
    

    以下、ビューファイル記述の際の注意点です。

  • モデルはFormオブジェクトのモデルを設定。
  • エラーメッセージの表示のモデルもFormオブジェクトのモデルを設定。(今回はブロック変数が定義されているためそれを使用)
  • urlは必ず記述。(手入力で生成したファイルのため、直接該当するURLがないから。)

    実装中に遭遇したエラーを紐解く

    今回は、住所やクレジットカード情報を保存するdeliverモデルと、商品情報とdeliver情報を紐づけるorder_itemモデルのふたつをFormオブジェクトを用いて保存していきました。

    ①paramsについて、MissingParamsやNoMethodErrorになる


    f:id:programmingnuoh:20210327172944p:plain 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メソッドを使っても見つけることができません。
    f:id:programmingnuoh:20210327174239p:plain

    f:id:programmingnuoh:20210327174317p:plain
    試行錯誤の嵐
    ・原因・
  • 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を取得したかったため、paramsmerge部分に記述されているカラム名を選択しています。

  • ②OrderItemの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
    

    そもそもの話ですが…

    Xxx.create(name: name,..........,user_id:user_id)
    

    このような記述があった場合、:の左側はテーブルのカラム名、右側はストロングパラメーターやattr_accecorメソッド、ビューなどに入力された値をそれぞれ指します。
    ここはなんとなくの私の認識なので間違っていたら訂正をお願いしたいのですが、
    user_id→カラム名とかform_with内とかで使用する名前で、値を入れる箱
    user.id→値そのもの。これをカラムとかの箱に入れる。
    このように認識しています。
    今回は、ビューファイル内等すべてuser_idで記述していたので、create()内の記述もこれに統一しました。


    まとめ

    Formオブジェクトを作成していく上では、モデル同士やモデルとコントローラー、ビューなどそれぞれの関係性をきちんと理解していないと記述できないな、と感じました。(だからエラー沼に落ちた…)
    流れの把握もそうなのですが、それぞれが値なのか、カラム名なのかという細かいところまで確認しながら実装していかないといけませんね。
    最初は時間がかかるかもしれませんが、徐々に慣れていきたいと思います。