🙆‍♀️

ActiveRecord::RecordInvalid: Validation failed: ~ must existの発生原因と解決方法

2023/06/03に公開

背景

rails でカート機能を作成しているとき、下記のエラーが出ました。

ActiveRecord::RecordInvalid: Validation failed: Cart item must exist.

(ActiveRecord::RecordInvalid: 検証に失敗しました: CartItemが存在する必要があります)

このエラーの原因と解決方法について解説します。

エラー部分

def create
  cart = current_api_v1_user.cart || Cart.create(user_id: current_api_v1_user.id)
  item = Item.find_by(name: params[:name])
  response = {}

  if item
    cart_item = CartItem.create(cart_id: cart.id, item_id: item.id, quantity: params[:quantity])
    
    # ここで発生。つまり、updateメソッドがうまく実行できていないということ。
    item.update(stock: item.stock - params[:quantity]) if cart_item.valid? 
    
    response[:cart] = cart
    response[:item] = item
    response[:message] = "商品がカートに正常に追加されました。"
  else
    response[:error] = "商品が見つかりません。"
  end

  render json: response, status: :ok
end

 
修正前の各モデル
cart.rb

class Cart < ApplicationRecord
  belongs_to :user
  has_many :cart_items, dependent: :destroy
end

cart_item.rb

class CartItem < ApplicationRecord
  belongs_to :cart
  belongs_to :item
end

item.rb

class Item < ApplicationRecord
  belongs_to :cart_item
end

 

エラー原因/解決方法

エラー原因は、belongs_to :cart_itemの関連付けで、存在しない cart_item を参照しようとしていたためです。

belongs_toのデフォルトでは関連するオブジェクトが存在しないとバリデーションエラーが発生します。

つまり、belongs_to :cart_item のデフォルトの振る舞いでは、関連するオブジェクト(ここでは CartItem)が存在しない場合、関連付けのバリデーションエラーが発生します。

しかし、optional: true オプション(アソシエーションによって紐づけられた外部キーの値が存在しない場合でも、データベースに保存することができるオプション)を使用することで、関連付けがオプショナル(任意)であることを示すことができます。

 
optional: true について
https://techtechmedia.com/optional-true-rails/

 
つまり、optional: true を設定することで、belongs_to 関連付けによるバリデーションがスキップされるようになり、関連する CartItem オブジェクトが存在しなくても、Item モデルの保存は成功します。

したがって、optional: true を追加したことで、Item モデルのバリデーションに関連する CartItem の存在が必須ではなくなり、一旦エラーが解決されました。

修正前

class Item < ApplicationRecord
  belongs_to :cart_item
end

修正後

class Item < ApplicationRecord
  belongs_to :cart_item, optional: true
end

ただ、belongs_to :cart_item, optional: trueを使用することでこのエラーは一旦解決はできますが、これは Cart と Item の多対多の関係を表現する上で適切ではありません。

つまり、現時点だと ItemからCart情報を参照、またはCartからItem情報を参照するための関連付けがなされていないため、ItemからCart情報を参照することやCartからItem情報を参照することができません。

(Cart.last.items や Item.first.carts をrais c で実行してもデータを取得できないということです。)

この状況を解決するには、CartItemモデルを介して ItemCart を関連付けるための
has_manythrough を使用する必要があります。これにより、カート内の複数のアイテムを管理することができます。

has_many と throughを使用し、修正したものが下記になります。

cart.rb

class Cart < ApplicationRecord
  belongs_to :user
  has_many :cart_items, dependent: :destroy
  has_many :items, through: :cart_items
end

cart_item.rb

class CartItem < ApplicationRecord
  belongs_to :cart
  belongs_to :item
end

item.rb

class Item < ApplicationRecord
  has_many :cart_items
  has_many :carts, through: :cart_items
end

修正後のコードでは、Itemモデルが複数のCartItemと関連付けられ、さらにそれらのCartItemを介して複数のCartと関連付けられるようになります。

つまり、CartからItemを参照することができるようになり、また、ItemからCartも参照することができるようになりました。

これにより、カートにアイテムを追加したり、アイテムをカートから削除したりすることが容易になります。

 

まとめ

ActiveRecord::RecordInvalid: Validation failed: ~ must exist. というエラーが発生した時、optional: true を使用すればこのエラーを解決できる記事をよく見ますが、今回の状況を考えるとケースバイケースのように思いました。

今回の場合、そもそもの関連付けが間違えていたので、その部分を適切な関連付けに修正することで optional: true を使用せずに済みました。

今後は、関連付けを行うときは適切に関連付けが行われているかということを確認しつつ、ActiveRecord::RecordInvalid: Validation failed: ~ must exist. というエラーが出たときは、optional: true を使用すべきかどうかを適宜考えていきます。

Discussion