はじめに
・商品を売ったり買ったりする場合のアソシエーションで悩みました。
・class_nameオプションを使うことで解決できたので、備忘録として書きます。
・自分の状況に落とし込んでいるので、わかりにくかったらすみません。
・間違いなどあればご指摘いただけますと幸いです。
・ミニアプリを作る手順はこちらの記事を見てください。
前提条件
・ユーザーがいます(usersテーブル)
・商品があります(itemsテーブル)
・商品が出品できる状況です(データ送信できる)
・1人のユーザーは複数の商品を出品(購入)することができます
・1つの商品は1人のユーザーに所属しています
→ユーザーと商品は1対多の関係です
class_nameって?
関連名を変更するために、クラス名を明示的に表示するオプションです。
たぶんどういう意味かあんまりわからないと思うので、具体例で説明します。
まず、以下のテーブルを見てください。
user has_many :items
item belongs_to :user
こんな関係性になってます。
・usersテーブル
| id | name |
|---|---|
| 1 | 山田 |
| 2 | 田中 |
| 3 | 鈴木 |
・itemsテーブル
| id | name | user_id |
|---|---|---|
| 1 | イモ | 1 |
| 2 | サケ | 1 |
| 3 | フグ | 2 |
山田さんはイモとサケを出品しています。
田中さんはフグを出品しています。
今、山田さんが出品したitemを全て取得することはできます。
イモを出品した人を取得することもできます。
user = User.find(1)
user.items
でも、誰がイモを買うのか?
このままでは判断できません。
1つのitemに対して買う人が1人いる。
今はitemsテーブルのid = 1(イモ)を出品した人が山田ということしか取得できません。
買った人を取得するためには、出品した人と買った人を参照できるようにする必要があります。
つまり、外部キー制約のついたカラムを置く必要があります。
・itemsテーブル
| id | name | 外部キー1 | 外部キー2 |
|---|---|---|---|
| 1 | イモ | 1 | 3 |
| 2 | サケ | 1 | 3 |
| 3 | フグ | 2 | 1 |
例えば、
外部キー1に出品する人、
外部キー2に買う人、
を持ってくればいいですね。
外部キー制約のカラムの認識は
「参照先のモデル名(小文字)」 + 「_id」
で認識されます。
わかりやすくするため、デフォルトの名前であるuser_idを使わないようにします。
・itemsテーブル
| id | name | seller_id | buyer_id |
|---|---|---|---|
| 1 | イモ | 1 | 3 |
| 2 | サケ | 1 | 3 |
| 3 | フグ | 2 | 1 |
integer型でseller_idカラムとbuyer_idカラムを追加しましょう。
ただしこのままでは認識されません
UserモデルとItemモデルのアソシエーションにforeign_keyオプションを使って、外部キーであることを明示的に宣言する必要があります。
ではこうしましょう
has_many :items, foreign_key: 'seller_id'
has_many :items, foreign_key: 'buyer_id'
belongs_to :user, foreign_key: 'seller_id'
belongs_to :user, foreign_key: 'buyer_id'
いつもはforeign_key: trueですけど今回は違います。
こういう名前にしたい!という名前をオプションで宣言します。
これで外部キーとして認識されました。
ですが、これではエラーになります
認識はされたものの、ユーザーに関連するitemsにアクセスする際、関連名が同一になっている(items)ため、出品したものを取得したいのか、買ったものを取得したいのかがわからないという状況になってしまっています。
→この状態でUserモデルのインスタンスに関連するitemsを取得しようとしてもエラーが出ます
エラーを解決するためにclass_nameオプションを使います
ここでclass_nameオプションが出てきます。
出品したものか、買ったものなのか、
あるいは
出品した人なのか、買った人なのか、
を判断するためにclass_nameオプションを使って、同一の関連名を変更します!!!
ここ大事!!!
Rails 5.1からの変更点として、belongs_toを指定した時、自動的にrequired: trueオプションが追加されpresenseのバリデーションが走るようになったようです(https://github.com/rails/rails/issues/34454 )。
そのため、今回のケースではbuyer_idのほうにoptional: trueをかけてpresenseのバリデーションを追加しないように指定することが必要です。
そうしないと、ユーザー登録や商品購入の際にbuyerを入力してくださいと言われちゃいます。
# sold_itemsは出品された商品にアクセスする関連名、bought_itemsは買った商品にアクセスする関連名です
# class_name: '関連するモデルのクラス名'
has_many :sold_items, class_name: 'Item', foreign_key: 'seller_id'
has_many :bought_items, class_name: 'Item', foreign_key: 'buyer_id'
# sellerは出品した人にアクセスする関連名、buyerは買った人にアクセスする関連名です
belongs_to :seller, class_name: 'User', foreign_key: 'seller_id'
belongs_to :buyer, class_name: 'User',foreign_key: 'buyer_id', optional: true
ということで、
usersテーブルのid: 1の山田が出品した商品と買った商品を取得することができるようになりました。
user = User.find(1)
User.sold_items
User.bought_items
itemsテーブルのid: 1のイモを出品した人と買った人を取得することができるようになりました。
item = Item.find(1)
Item.seller
Item.buyer
実際にミニアプリを作って試してみる時の注意点
・コントローラーへの記述でprivateメソッドの中にストロングパラメーターの記述をすると思いますが、出品者のidを取得する場合は.merge(seller_id: current_user.id)と記述すると良いかと思います。購入者の場合はseller_idをbuyer_idに変更すれば良いかと思います。
まとめ
・売る人と買う人がいる場合はclass_nameオプションを使うと実装しやすいと思います。
・ミニアプリを作る手順はこちらの記事を見てください。
参考
【Rails】アソシエーションを図解形式で徹底的に理解しよう!
https://pikawaka.com/rails/association#class_name%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3
Ruby on Rails アソシエーションの応用 class_name 【一つのモデルを複数に分岐する】
https://qiita.com/mylevel/items/421cc1cd2eb5b39e20ad