LoginSignup
159
186

More than 5 years have passed since last update.

【Rails】テーブル間の条件付きアソシエーションの設定【メルカリコピー作成記】

Last updated at Posted at 2017-06-04

メルカリのコピーを作成しているんだがもう俺は限界かもしれない。

現在私はフリマアプリ「メルカリ」のコピーをRuby on Rails5で作成しています。
開幕早々、「DB設計」「テーブル間の条件付きリレーション」でもう俺は限界かもしれない状況に追い込まれました。

スクリーンショット 2017-06-04 19.26.44.png

メルカリはフリマアプリなので、ユーザーが商品を「出品する」と「購入する」という機能が必要です。
この2つの機能を実装していくにあたっての登場人物は大きく2つです。
(コメント・メッセージ機能等は割愛しています。)

  • ユーザー → Userモデルを作成
  • 商品 → Itemモデルを作成

これは想像に容易いのですが、メルカリには下記のような概念がありました。

  • Userといっても「購入者」と「出品者」って概念があるじゃん。
  • Itemといっても「既に売った商品」「現在まだ売っている商品」「買った商品」があるじゃん。

これどうすんのよ!
ということで、「DB設計」と「テーブル間のリレーションの組み方」に非常に苦しんだので、その解決の過程をメモしておきます。
「テーブル間の条件付きリレーションの設定方法」に苦しんでいる方には、もしかしたら参考になる部分があるかもしれません。

itemsテーブルに「buyer_id」「saler_id」を追記する

まず、「購入者」「出品者」の概念を作る必要があるので、itemsテーブルに「buyer_id」「saler_id」をintegerとして追記しました。

スクリーンショット 2017-06-04 19.30.45.png

「buyer」とか「saler」とかどっから出て来たんだよって話ですが、この後にitemsテーブルとusersテーブルのアソシエーションを組む際に設定します。
ちなみに、DB内の初期データはgemのseed-fuを用いて適当に3レコードくらい作りました。

userデータの中身も同様に4人分くらいのレコードを作成しました。

スクリーンショット 2017-06-04 21.55.47.png

今回のDB設計

一応、現在どういう状況かの参考までにDBカラム一覧置いておきます。

usersテーブル itemsテーブル  
id id
nickname name
email image
password state(商品の状態)
password_confirmation postage(配送料の負担)
・・・ region(発送元地域)
shipping_date(発送までの日数)
price(価格)  
saler_id (出品したuserのid)
buyer_id (購入したuserのid)

Itemモデルで 「購入者」「出品者」を取り出せるようにする。

Itemモデルに下記のようなアソシエーションを組みました。

models/item.rb
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モデルにて下記のようにアソシエーションを組むことで実現できました。

models/user.rb
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

models/user.rb
has_many :buyed_items, foreign_key: "buyer_id", class_name: "Item"

itemsテーブルの「buyer_id」(購入者)とuserの「id」と紐かせることで、userが買った商品を全て取り出すことができます。

userが 「現在売っている」商品 →saling_items

models/user.rb
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

models/user.rb
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>]

無事、全てのアソシエーションが設定できました!

159
186
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
159
186