ActiveRecord::RecordInvalid: Validation failed: ~ must existの発生原因と解決方法
背景
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 について
つまり、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
モデルを介して Item
と Cart
を関連付けるための
has_many
と through
を使用する必要があります。これにより、カート内の複数のアイテムを管理することができます。
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