【Rails】自己結合関連付けでお酒の順番を表現してみた。

本記事は RUNTEQアドベントカレンダー 2021 の11日目の記事となります！

現在私は、一軒目で飲むお酒の順番をユーザーに提供するアプリを作成しています！
今回は、「お酒の順番をまとめたセット」と「お酒単品」を表すに際に学習した、「自己結合関連付け」に関してを書かせていただきます！
（以降、一軒目で飲むお酒の順番は「酒ケジュール」という言葉で表させていただきます。）



【この記事を読んだら】
- 自己結合関連付を理解することができます
- 酒ケジュールを作成することができます。
[![Image from Gyazo](https://i.gyazo.com/68521eedf0b6685e1894f96445fe726d.jpg)](https://gyazo.com/68521eedf0b6685e1894f96445fe726d)

## 自己結合関連付とは？

>同一テーブル内のカラムを関連づけるための処理のこと。

一つのテーブルの中で多対多の関連付けをする際に用いられている表現を指します。


身近な例で言うと、TwitterにおけるFollow,Followedの関係も自己結合関連づけになります。

この関係を分解すると、以下の通りになります。

- Userは他のUserをフォローできる
- Userは他のUserからフォローされる

英語で表現してみましょう。

```ruby
a User follows User
a User is follwed by User
```

同一テーブル間でリレーションが組まれていますね。

このUser同士をくっつけるために、Usersテーブルの外部キーのみを保存したRelationshipsテーブルを作成します。

```ruby
User -< Relationships >- User
```

こうして出来上がった関連付けの形を**自己結合関連付け**と言います。

### 自己結合関連付けのタイプ

自己結合は以下の2つに分類されます。

- 中間テーブルを介さない純粋な自己結合関連付け
- 中間テーブルを介した自己結合関連付け

酒ケジュールを取得するには後者の、「中間テーブルを介した自己結合関連付け」を採用しようと思います。

それでは実際に酒ケジュールを作成してみましょう。

### 酒ケジュール作成の全体像

1. AlcoholsテーブルとRelationshipsテーブルを用意する
2. alcohols_controllerを用いて酒ケジュールを作成する
3. 酒ケジュールを画面に表示する

### AlcoholsテーブルとRelationshipsテーブルを用意する

ER図は以下の通りです。

[![Image from Gyazo](https://i.gyazo.com/04e6f2534f36519a0b9dfec2e1916e0d.png)](https://gyazo.com/04e6f2534f36519a0b9dfec2e1916e0d)

**Alcoholsテーブル**

アルコールの順番を保存するボックス（酒ケジュール）と保存されるアルコール(単品）を用意するテーブルになります。

自己結合関連付けはその名の通り、自分同士を繋げることになります。

名前の衝突を回避するために、自己結合を行いたいテーブル内に仮のカラムを二つ用意する必要が出てきます。

今回は仮のカラムを下記のように命名しています。

liquor: お酒（単品）

liquor_box: 酒ケジュール（単品のお酒を４つ格納）

そしてliquorとliquor_boxを関連づけるために、relationshipsテーブルにも下記のような仮のカラム二つを用意してあげます。

liquor_id: liquorの外部キー

liquor_box_id: liquor_boxの外部キー

**alcohol.rb**

```ruby
class Alcohol < ApplicationRecord

  # 自己結合

  # 12500ボックス(酒ケジュール)に属する　liquor_id(お酒)を取得するassociation
  has_many :liquor_relationships,
           class_name: 'Relationship',
           foreign_key: 'liquor_box_id',
           dependent: :destroy,
           inverse_of: :liquor

  # お酒が属するliquor_box_id(酒ケジュール)を取得するassociation
  has_many :liquor_box_relationships,
           class_name: 'Relationship',
           foreign_key: 'liquor_id',
           dependent: :destroy,
           inverse_of: :liquor_box

  # 【後段部分】

  # 1000ボックスから焼酎、日本酒、ワインを取得する
  has_many :liquors, through: :liquor_relationships

  # ビールから1000ボックスを取得する
  has_many :liquor_boxes, through: :liquor_box_relationships
end
# throughオプションによりliquor_relationships経由でliqur_boxesにアクセスできるようになる
# alcohol.alcohol_boxesで酒ケジュールにアクセスができる
```

has_many throughアソシエーションを使い、liquorとliquorボックスを関連づけています。

両者ともにrelationshipsを通して結びつけたいが、そのままだと名前の衝突が起きてしまいます。

この問題は、後ろにclass_name: 'Relationship'をつけ、それぞれ別の名前をつけることで解消することができます。

```ruby
 has_many :liquor_relationships,
 class_name: 'Relationship',

 has_many :liquor_box_relationships,
 class_name: 'Relationship',
```

### **Relationshipsテーブル**:

Relationshipsテーブルは、Alcoholsテーブル内の「アルコールセット(酒ケジュール）」と「アルコール（酒単品）」を結びつける中間テーブルになります。

中間テーブルなので、結びつけたい対象の外部キーのみを保存してあります。

**relationship.rb**

```ruby
class Relationship < ApplicationRecord
  belongs_to :liquor, class_name: 'Alcohol'
  belongs_to :liquor_box, class_name: 'Alcohol'
end
```

お酒データと酒ケジュールデータを予めseed_fuで作成しておきます。

**お酒データ**

**seeds/01_alcohol.rb**

```ruby
Alcohol.seed(
   {
        name: '12500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 12500,
        description: 'アルコール総量が12500mlのボックスです。'
    },
    {
        name: '12000ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 12000,
        description: 'アルコール総量が12000mlのボックスです。'
    },
    {
        name: '11500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 11500,
        description: 'アルコール総量が11500mlのボックスです。'
    },
    {
        name: '11000ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 11000,
        description: 'アルコール総量が11000mlのボックスです。'
    },
    {
        name: '10500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 10500,
        description: 'アルコール総量が10500mlのボックスです。'
    },
    {
        name: '10000ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 10000,
        description: 'アルコール総量が10000mlのボックスです。'
    },
    {
        name: '9500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 9500,
        description: 'アルコール総量が9500mlのボックスです。'
    },
    {
        name: '9000ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 9000,
        description: 'アルコール総量が9000mlのボックスです。'
    },
    {
        name: '8500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 8500,
        description: 'アルコール総量が8500mlのボックスです。'
    },
    {
        name: '8000ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 8000,
        description: 'アルコール総量が8000mlのボックスです。'
    },
    {
        name: '7500ボックス',
        alcohol_percentage: 0,
        alcohol_amount: 7500,
        description: 'アルコール総量が7500mlのボックスです。'
    },
    {
        name: 'ビール',
        alcohol_percentage: 5.00,
        alcohol_amount: 350,
        description: '「醸造酒」の一つ。とりあえずビールでお馴染みのビール。乾杯、そしてキンキンに冷えたビールを流し込もう。',
        image: File.open('db/fixtures/images/beer.png')
    },
    {
        name: 'ビール(一口)',
        alcohol_percentage: 5.00,
        alcohol_amount: 350,
        description: '「醸造酒」の一つ。とりあえずビールの風潮で頼んだはいいがビールがそこまで好きではないあなたへのジャストサイズ。',
        image: File.open('db/fixtures/images/jokki_beer.jpg')
    },
    {
        name: 'ザ・プレミアムモルツ',
        alcohol_percentage: 5.00,
        alcohol_amount: 350,
        description: '「醸造酒」の一つ。きめ細やかな泡。乾杯、そしてキンキンに冷えたビールを流し込もう。',
        image: File.open('db/fixtures/images/premol.jpg')
    },
   {
        name: "赤ワイン",
        alcohol_percentage: 12.00,
        alcohol_amount: 120,
        description: "「醸造酒」の一つ。ヨーロッパで嗜まれている赤葡萄仕立てのお酒。洋風の店に置いてあることが多い、大人の味。肉によく合う。",
        image: File.open('db/fixtures/images/wine_bottle.jpg')
    },
   {
        name: "白ワイン",
        alcohol_percentage: 12.00,
        alcohol_amount: 120,
        description: "「醸造酒」の一つ。ヨーロッパで嗜まれている白葡萄仕立てのお酒。洋風の店に置いてあることが多い、大人の味。魚によく合う。",
        image: File.open('db/fixtures/images/wine_bottle.jpg')
    },
    {
        name: "ジンソーダ",
        alcohol_percentage: 6.00,
        alcohol_amount: 350,
        description: "ジンをソーダで割ったライム風味のお酒。爽やかなライムの香りが特徴。炭酸のシュワシュワと爽快な味わいが特徴。",
        image: File.open('db/fixtures/images/jin_tonic.png')
    },
    {
        name: "ジントニック",
        alcohol_percentage: 5.00,
        alcohol_amount: 350,
        description: "ジンを水で割ったライム風味のお酒。爽やかなライムの香りが特徴。コスパ良く酔いたい時におすすめ。",
        image: File.open('db/fixtures/images/jin_tonic.png')
    },
)
```

**酒ケジュールデータ**

**seeds/02_relationship.rb**

```ruby
Relationship.seed(
        {
            liquor_box_id: 1,
            liquor_id: 25
        },
        {
            liquor_box_id: 1,
            liquor_id: 64
        },
        {
            liquor_box_id: 1,
            liquor_id: 38
        },
        {
            liquor_box_id: 1,
            liquor_id: 28
        },
        {
            liquor_box_id: 2,
            liquor_id: 25
        },
        {
            liquor_box_id: 2,
            liquor_id: 64
        },
        {
            liquor_box_id: 2,
            liquor_id: 46
        },
        {
            liquor_box_id: 2,
            liquor_id: 42
        },
        {
            liquor_box_id: 3,
            liquor_id: 64
        },
        {
            liquor_box_id: 3,
            liquor_id: 46
        },
        {
            liquor_box_id: 3,
            liquor_id: 41
        },
        {
            liquor_box_id: 3,
            liquor_id: 42
        },
```

### Relationshipsテーブルを解説

自己結合の考え方を使って、酒ケジュールとお酒を関連づけています。

12500**ボックス(liquor_box_id=1)に紐づけられたお酒**

- ビール(liquor_id=25)
- メガハイボール(liquor_id=64)
- 芋焼酎お湯割り(liquor_id=38)
- 日本酒(liquor_id=28)

12000**ボックス(liquor_box_id=2)に紐づけられたお酒**

- ビール(liquor_id=25)
- メガハイボール(liquor_id=64)
- ウィスキーストレート(liquor_id=46)
- ハイボール濃いめ(liquor_id=42)

11500ボックス**(liquor_box_id=3)に紐づけられたお酒**

- メガハイボール(liquor_id=64)
- ウィスキーストレート(liquor_id=46)
- ウィスキーロック(liquor_id=41)
- ハイボール濃いめ(liquor_id=42)

## alcohols_controllerを用いて酒ケジュールを作成する

　ModelとViewの橋渡し役であるControllerに酒ケジュール作成のロジックを書いていきます。

api/v1/alcohols_controller.rb

```ruby
module Api
  module V1
    class AlcoholsController < ApplicationController
      def new
        Alcohol.new
      end

      def index
        alcohols = Alcohol.all
        alcohols_names = alcohols.map(&:liquors)
        alcohols_json = {}
        alcohols_names.each_with_index do |name, index|
          json_key = "alcohols_#{index + 1}"
          alcohols_json[json_key] = name
        end
        respond_to { |format| format.json { render json: alcohols_json, methods: [:image_url] } }
      end

      def create
        @alcohol = Alcohol.build(alcohol_params)
        if alcohol.save
          render json: @alcohol, status: :created, methods: [:image_url]
        else
          render json: @alcohol.errors.full_messages, status: :bad_request
        end
      end

      private

      def alcohol_params
        params
          .require(:alcohol)
          .permit(
            :type,
            :alcohol_percentage,
            :alcohol_amount,
            :name,
            :description,
            :pure_alcohol_intake,
            :image
          )
      end
    end
  end
end
```

createアクションでデータを作成し、indexアクションでviewに表示させる処理を行なっています。

indexアクションを具体的に見ていきます。

```ruby
 1  def index
 2      alcohols = Alcohol.all
 3      alcohols_names = alcohols.map(&:liquors)
 4      alcohols_json = {}
 5      alcohols_names.each_with_index do |name, index|
 6        json_key = "alcohols_#{index + 1}"
 7        alcohols_json[json_key] = name
 8     end
 9      respond_to { |format| format.json { render json: alcohols_json} }
      end
```

2段目 

　データベースに保存してあるAlcoholデータを全件取得しています。

3段目~8段目 

　取得したAlcoholデータを4段目のオブジェクトに繰り返し処理であるeach_with_indexを用いて代入しています。

9段目

　作成したオブジェクトをjson形式でレスポンスさせています。

### 実際に発行されるSQL

実際にindexアクションを実行すると、下記画像のに、酒ケジュールに紐づいたお酒（一セットあたり4本）を取得する処理が、indexの数だけ実行されます。

[![Image from Gyazo](https://i.gyazo.com/53eae5c0c71d9b2fa0a75c751dcd3f04.png)](https://gyazo.com/53eae5c0c71d9b2fa0a75c751dcd3f04)

### 酒ケジュールを作成する準備が整った

seedファイル、alcoholsコントローラーのおかげで、酒ケジュール作成に必要なデータ、及びコントローラーが整いました。

実際にalcohols_controllerを使いAPIを叩くと下記のようなデータにアクセスすることができます。
![d58bb983906d0efd9df43a52590f5f6a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1466226/9d176208-e364-ea46-fd52-22c4ee0afb30.png)

## 酒ケジュールを画面に表示する

コントローラーが完成したことにより、apiに酒ケジュールを保存させることができました。

今回はVue.jsを使い、DBに保存されている酒ケジュールをviewに持ってきます。





### pages/Result.vue

酒ケジュールが表示される画面になります。

**template部分**

```ruby
<template>
<v-col cols="12" sm="3" class="d-flex" v-for="data in contents" :key="data.id">
              <v-card
                class="text-center mx-auto my-5 form"
                elevation="2"
                width="100%"
                shaped
                id="form"
              >
                <v-icon :logo="data.percentage === 0 ? 'mdi-cup' : 'mdi-glass-mug'">
                  mdi-glass-mug</v-icon
                >
                <v-card-title style="width: 100%" class="headline justify-center">
                  {{ data.name }}
                </v-card-title>
                <v-row justify="center" align-content="center">
                  <p>度数: {{ data.alcohol_percentage }}%</p>
                  <p>量: {{ data.alcohol_amount }}ml</p>
                </v-row>
              </v-card>
            </v-col>
</template>
```

**script部分**

DOMが構築された後、mountedにおいて、alcohols_controllerのindexアクションを実行して先程作成した酒ケジュールデータをフロント側に持ってきています。

```ruby
<script>
import { mapActions, mapGetters, mapMutations } from 'vuex';
import axios from '../plugins/axios';
export default {
  data: function () {
    return {
      alcohols: [],
      analyze: [],
      users: [],
    };
  },

  computed: {
    ...mapGetters('analyze', ['analyzes']),
    ...mapGetters('users', ['authUser']),
    contents() {
      const thisAnalyze = this.analyzes;

      const analyzeShuchedule = thisAnalyze[thisAnalyze.length - 1]['shuchedule'];

      const targetValues = this.alcohols;

      const contentsOfTarget = Object.values(targetValues)[analyzeShuchedule];

      return contentsOfTarget;
    },
  },
  mounted() {
    axios.get('/alcohols').then((alcoholResponse) => (this.alcohols = alcoholResponse.data));
    this.analyze = this.fetchAnalyzes;
  },
  updated() {},
  created() {
    this.fetchAnalyzes();
    this.fetchAuthUser();
  },
  methods: {
    ...mapActions('analyze', ['fetchAnalyzes']),
    ...mapActions('users', ['fetchAuthUser']),
  },
};
</script>

<style scoped>
#izakaya {
  background: url(../src/img/beer.jpeg) center center / cover no-repeat fixed;
}
</style>
```

持ってきたデータはcomputed内に書かれた算出メソッドcontentsで加工され、リストレンダリングを使い、viewで表示されています。

## 最終的に表示される酒ケジュール

最終的に表示される酒ケジュールはこちらになります

[![Image from Gyazo](https://i.gyazo.com/3b262a59fa414806c141b9d395730b8a.jpg)](https://gyazo.com/3b262a59fa414806c141b9d395730b8a)

### まとめ

- 自己関連結合とは、同一テーブル内に仮のカラムを二つ作って仮のカラム同士を繋げることを指す。

### 終わりに
最後までお読みいただきありがとうございました！
記事で分かりにくい箇所や過不足、誤りなどあればコメントいただけると幸いです！
本記事で使用しているコードは一部抜粋したものになります！
なので、より詳細を知りたいという方は[Github](https://github.com/subaru-hello/Zeroken)をご覧ください！


### 参考URL
[Active Record の関連付け - Railsガイド](https://railsguides.jp/association_basics.html#%E8%87%AA%E5%B7%B1%E7%B5%90%E5%90%88)


[自己結合とは｜「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word15317.html)

[自己結合を使って、カレーセットとカレーを関係付けてみた（フォローのassociationの勉強になるかも） | TechEssentials](https://tech-essentials.work/course_outputs/195)