本記事は RUNTEQアドベントカレンダー 2021 の11日目の記事となります!
現在私は、一軒目で飲むお酒の順番をユーザーに提供するアプリを作成しています!
今回は、「お酒の順番をまとめたセット」と「お酒単品」を表すに際に学習した、「自己結合関連付け」に関してを書かせていただきます!
(以降、一軒目で飲むお酒の順番は「酒ケジュール」という言葉で表させていただきます。)
【この記事を読んだら】
自己結合関連付とは?
同一テーブル内のカラムを関連づけるための処理のこと。
一つのテーブルの中で多対多の関連付けをする際に用いられている表現を指します。
身近な例で言うと、TwitterにおけるFollow,Followedの関係も自己結合関連づけになります。
この関係を分解すると、以下の通りになります。
- Userは他のUserをフォローできる
- Userは他のUserからフォローされる
英語で表現してみましょう。
a User follows User
a User is follwed by User
同一テーブル間でリレーションが組まれていますね。
このUser同士をくっつけるために、Usersテーブルの外部キーのみを保存したRelationshipsテーブルを作成します。
User -< Relationships >- User
こうして出来上がった関連付けの形を自己結合関連付けと言います。
自己結合関連付けのタイプ
自己結合は以下の2つに分類されます。
- 中間テーブルを介さない純粋な自己結合関連付け
- 中間テーブルを介した自己結合関連付け
酒ケジュールを取得するには後者の、「中間テーブルを介した自己結合関連付け」を採用しようと思います。
それでは実際に酒ケジュールを作成してみましょう。
酒ケジュール作成の全体像
- AlcoholsテーブルとRelationshipsテーブルを用意する
- alcohols_controllerを用いて酒ケジュールを作成する
- 酒ケジュールを画面に表示する
AlcoholsテーブルとRelationshipsテーブルを用意する
ER図は以下の通りです。
Alcoholsテーブル
アルコールの順番を保存するボックス(酒ケジュール)と保存されるアルコール(単品)を用意するテーブルになります。
自己結合関連付けはその名の通り、自分同士を繋げることになります。
名前の衝突を回避するために、自己結合を行いたいテーブル内に仮のカラムを二つ用意する必要が出てきます。
今回は仮のカラムを下記のように命名しています。
liquor: お酒(単品)
liquor_box: 酒ケジュール(単品のお酒を4つ格納)
そしてliquorとliquor_boxを関連づけるために、relationshipsテーブルにも下記のような仮のカラム二つを用意してあげます。
liquor_id: liquorの外部キー
liquor_box_id: liquor_boxの外部キー
alcohol.rb
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'をつけ、それぞれ別の名前をつけることで解消することができます。
has_many :liquor_relationships,
class_name: 'Relationship',
has_many :liquor_box_relationships,
class_name: 'Relationship',
Relationshipsテーブル:
Relationshipsテーブルは、Alcoholsテーブル内の「アルコールセット(酒ケジュール)」と「アルコール(酒単品)」を結びつける中間テーブルになります。
中間テーブルなので、結びつけたい対象の外部キーのみを保存してあります。
relationship.rb
class Relationship < ApplicationRecord
belongs_to :liquor, class_name: 'Alcohol'
belongs_to :liquor_box, class_name: 'Alcohol'
end
お酒データと酒ケジュールデータを予めseed_fuで作成しておきます。
お酒データ
seeds/01_alcohol.rb
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
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
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アクションを具体的に見ていきます。
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の数だけ実行されます。
酒ケジュールを作成する準備が整った
seedファイル、alcoholsコントローラーのおかげで、酒ケジュール作成に必要なデータ、及びコントローラーが整いました。
実際にalcohols_controllerを使いAPIを叩くと下記のようなデータにアクセスすることができます。
酒ケジュールを画面に表示する
コントローラーが完成したことにより、apiに酒ケジュールを保存させることができました。
今回はVue.jsを使い、DBに保存されている酒ケジュールをviewに持ってきます。
pages/Result.vue
酒ケジュールが表示される画面になります。
template部分
<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アクションを実行して先程作成した酒ケジュールデータをフロント側に持ってきています。
<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で表示されています。
最終的に表示される酒ケジュール
最終的に表示される酒ケジュールはこちらになります
まとめ
- 自己関連結合とは、同一テーブル内に仮のカラムを二つ作って仮のカラム同士を繋げることを指す。
終わりに
最後までお読みいただきありがとうございました!
記事で分かりにくい箇所や過不足、誤りなどあればコメントいただけると幸いです!
本記事で使用しているコードは一部抜粋したものになります!
なので、より詳細を知りたいという方はGithubをご覧ください!
参考URL
Active Record の関連付け - Railsガイド
自己結合とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
自己結合を使って、カレーセットとカレーを関係付けてみた(フォローのassociationの勉強になるかも) | TechEssentials