メルカリのコピーを作成しているんだがもう俺は限界かもしれない。
現在私はフリマアプリ「メルカリ」のコピーをRuby on Rails5で作成しています。
開幕早々、***「DB設計」と「テーブル間の条件付きリレーション」***でもう俺は限界かもしれない状況に追い込まれました。
メルカリはフリマアプリなので、ユーザーが商品を「出品する」と「購入する」という機能が必要です。
この2つの機能を実装していくにあたっての登場人物は大きく2つです。
(コメント・メッセージ機能等は割愛しています。)
- ユーザー → Userモデルを作成
- 商品 → Itemモデルを作成
これは想像に容易いのですが、メルカリには下記のような概念がありました。
- Userといっても「購入者」と「出品者」って概念があるじゃん。
- Itemといっても「既に売った商品」「現在まだ売っている商品」「買った商品」があるじゃん。
これどうすんのよ!
ということで、「DB設計」と「テーブル間のリレーションの組み方」に非常に苦しんだので、その解決の過程をメモしておきます。
**「テーブル間の条件付きリレーションの設定方法」**に苦しんでいる方には、もしかしたら参考になる部分があるかもしれません。
itemsテーブルに「buyer_id」「saler_id」を追記する
まず、「購入者」「出品者」の概念を作る必要があるので、itemsテーブルに**「buyer_id」「saler_id」**をintegerとして追記しました。
「buyer」とか「saler」とかどっから出て来たんだよって話ですが、この後にitemsテーブルとusersテーブルのアソシエーションを組む際に設定します。
ちなみに、DB内の初期データはgemのseed-fuを用いて適当に3レコードくらい作りました。
userデータの中身も同様に4人分くらいのレコードを作成しました。
今回のDB設計
一応、現在どういう状況かの参考までにDBカラム一覧置いておきます。
usersテーブル | itemsテーブル |
---|---|
id | id |
nickname | name |
image | |
password | state(商品の状態) |
password_confirmation | postage(配送料の負担) |
・・・ | region(発送元地域) |
shipping_date(発送までの日数) | |
price(価格) | |
saler_id (出品したuserのid) | |
buyer_id (購入したuserのid) |
Itemモデルで 「購入者」「出品者」を取り出せるようにする。
Itemモデルに下記のようなアソシエーションを組みました。
class Item < ApplicationRecord
belongs_to :saler, class_name: "User"
belongs_to :buyer, class_name: "User"
end
この記述により、**userテーブルの「id」とitemsテーブルの「buyer_id」「saler_id」**が紐づくようになります。
試しにターミナルにてrails c
コマンドを実行してみます。設定した「saler」と「buyer」を用いて、商品(Item)に紐づく「出品者」と「購入者」を取り出せるのか試してみました。
うまく設定できたか確認してみる
Item側から出品者「saler」は取り出せるのか?の確認
pry(main)> Item.find(2).saler
Item Load (0.5ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 2 LIMIT 1
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
=> #<User id: 3, email: "yyy@gmail.com", created_at: "2017-06-04 06:03:54", updated_at: "2017-06-04 06:09:19", nickname: "やまみち">
Item側から購入者「buyer」は取り出せるのか?の確認
pry(main)> Item.find(2).buyer
Item Load (0.4ms) SELECT `items`.* FROM `items` WHERE `items`.`id` = 2 LIMIT 1
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1
=> #<User id: 4, email: "fff@gmail.com", created_at: "2017-06-04 06:09:19", updated_at: "2017-06-04 06:09:19", nickname: "たなか">
無事、商品(Item)に紐づく出品者と購入者が取り出すことができました。
Userモデルで「買った商品」「現在売っている商品」「既に売った商品」を取り出せるようにする。
結論、Userモデルにて下記のようにアソシエーションを組むことで実現できました。
class User < ApplicationRecord
has_many :buyed_items, foreign_key: "buyer_id", class_name: "Item"
has_many :saling_items, -> { where("buyer_id is NULL") }, foreign_key: "saler_id", class_name: "Item"
has_many :sold_items, -> { where("buyer_id is not NULL") }, foreign_key: "saler_id", class_name: "Item"
end
こちらでは
- userが「買った」商品 →buyed_items
- userが 「現在売っている」商品 →saling_items
- userが「既に売った」商品 →sold_items
としています。
userが「買った」商品 →buyed_items
has_many :buyed_items, foreign_key: "buyer_id", class_name: "Item"
itemsテーブルの「buyer_id」(購入者)とuserの「id」と紐かせることで、userが買った商品を全て取り出すことができます。
userが 「現在売っている」商品 →saling_items
has_many :saling_items, -> { where("buyer_id is NULL") }, foreign_key: "saler_id", class_name: "Item"
これこそ理解に苦しんだ**「テーブル間の条件付きアソシエーション」**です。
**「現在売っている商品」**とは言うなれば...
「購入者(buyer_id)がまだ存在しない商品」であり、かつ、saler_idがuserのidと紐づいている商品
といえます。それを表現したのが上記コードというわけでした。
なお、whereでの条件指定()の中身はSQL文を用いており、「buyer_idがNULLのとき」を表現しています。
userが「既に売った」商品 →sold_items
has_many :sold_items, -> { where("buyer_id is not NULL") }, foreign_key: "saler_id", class_name: "Item"
これも先ほど同様にテーブル間の条件付きアソシエーションです。
**「既に売った商品」**とは言うなれば...
購入者(buyer_id)が既に存在している商品」であり、かつ、saler_idがuserのidと紐づいている商品
なので、これを表現したのが上記コードというわけでした。
whereの条件指定の中身は同様にSQL文を用いており、「buyer_idがNULLではないとき」を表現しています。
うまく設定できたか確認してみる
最後に、それぞれうまく設定できたかをターミナルにてrails c
コマンドを実行して確認します。
User側から「買った商品」buyed_itemsは取り出せるのか?の確認
pry(main)> User.find(2).buyed_items
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
Item Load (0.5ms) SELECT `items`.* FROM `items` WHERE `items`.`buyer_id` = 2
=> [#<Item:0x007f97e6d2b470
id: 1,
name: "あめ",
image: "aaa.img",
description: "オススメ商品です。",
state: "新品、未使用",
postage: "送料込み",
region: "北海道",
shipping_date: "2〜3日",
price: 1000,
saler_id: 1,
buyer_id: 2,
created_at: Sun, 04 Jun 2017 05:53:59 UTC +00:00,
updated_at: Sun, 04 Jun 2017 05:57:16 UTC +00:00>]
User側から「現在売っている商品」saling_itemsは取り出せるのか?の確認
pry(main)> User.find(1).saling_items
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
Item Load (0.4ms) SELECT `items`.* FROM `items` WHERE `items`.`saler_id` = 1 AND (buyer_id is NULL)
=> [#<Item:0x007f97e76470e0
id: 3,
name: "ぬいぐるみ",
image: "aaa.img",
description: "すばらしい商品です。",
state: "新品、未使用",
postage: "送料込み",
region: "北海道",
shipping_date: "2〜3日",
price: 1200,
saler_id: 1,
buyer_id: nil,
created_at: Sun, 04 Jun 2017 05:57:16 UTC +00:00,
updated_at: Sun, 04 Jun 2017 05:57:16 UTC +00:00>]
User側から「既に売った商品」sold_itemsは取り出せるのか?の確認
pry(main)> User.find(3).sold_items
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
Item Load (0.4ms) SELECT `items`.* FROM `items` WHERE `items`.`saler_id` = 3 AND (buyer_id is not NULL)
=> [#<Item:0x007f97e899a6b0
id: 2,
name: "かさ",
image: "aaa.img",
description: "いい商品です。",
state: "新品、未使用",
postage: "送料込み",
region: "北海道",
shipping_date: "2〜3日",
price: 800,
saler_id: 3,
buyer_id: 4,
created_at: Sun, 04 Jun 2017 05:57:16 UTC +00:00,
updated_at: Sun, 04 Jun 2017 05:57:16 UTC +00:00>]
無事、全てのアソシエーションが設定できました!