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

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

<img width="550" alt="スクリーンショット 2017-06-04 19.26.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/179510/ae30da82-d342-dc7c-2e3c-683afb816312.png">

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

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

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

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

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

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

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

<img width="1284" alt="スクリーンショット 2017-06-04 19.30.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/179510/073d0915-411a-27aa-d33f-70840827fda7.png">

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

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

<img width="507" alt="スクリーンショット 2017-06-04 21.55.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/179510/e62b2e31-f11a-7bd7-7536-0acf83e64610.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>]
```

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