RailsSQl

SQLを勉強して、ActiveRecord触ると本当に神だと思うようになる。

こんにちは。 未経験からPGに転職して、もうすぐ半年が経つ筆者です。

筆者はわりかし小さい会社に入ったため、同じ年頃の同僚や、年上の未経験からの転職者などと仕事をすることがありますが、なぜかその辺りの人の教育的なことをさせてもらっています。 (腐らずに仕事をし、ちゃんと挨拶をし、真面目に勉強していれば、なぜか勝手に評価され、信頼されすぎて逆にビビっています。😢)

今回は、sqlで利用するテーブル同士の結合について、ActiveRecordなら短いコードで実現できるというお話です。

単純な例

仮に、次のようなテーブルがあるとします。

受付情報テーブル 1 - 多 受付対象者情報テーブル 1 - 多 購入商品テーブル

モデル名(テーブル名)

Uketuke(uketukes) 1 - * Taisho(taishos) 1 - * Shohin(shohins)

まぁテーブル名にセンスがないとかそのようなことは置いておいて、

要するに、 受付情報テーブルに、対象者の情報と、対象者が購入した商品がぶら下がっているような形になります。

受付情報テーブルには、受付した日時、受付時に言われた要望などの情報を格納しています。 受付対象者情報テーブルには、商品を購入した対象者の情報を格納しています。(住所、生年月日、氏名など) 購入商品テーブルには、購入した商品の情報を格納しています。(購入した商品の商品コード、購入数量など)

ここで、 「ある特定の受付情報から、対象者の情報、商品の情報を取得する」という課題に対して、ものすごく遠回りをした書き方がこちら。(初心者の時にやってました)

#controller

uketuke = Uketuke.find(1) #id(管理番号で受付情報を取得)
taishos = Taisho.find_by(uketuke_id:uketuke.id)#受付の管理番号から対象者の情報を取得
shouhins = []#空の配列を用意して・・・

taishos.each do |taisho|
    elm = Shohin.find_by(taisho_id:)
    shouhins.push(elm) 
end

動く。動くよ。確かに。コイツァ動く。

確かにこれで受付に紐づく商品の一覧が取得できます。

ですが、本番の時のサーバー側への負担と、書く側が楽をするという点に考慮していれば、もっと良い方法があります。

まず、このコードだと受付に紐づくtaishoを見つけるのに1回、それに紐づく商品を見つけるのにn回sqlが発行されます。開発用のDBでは数件のデータで済みますが、本番では膨大な数のDBからSQLが発行されると思います。それを意識した上で、一回のsqlの発行で、対象の商品一覧が取れたらDB的にも、書く側としても楽になります。

SQL的にはこんな感じ。


SELECT u.*,t.*,s.*
FROM uketukes u
INNER JOIN taishos t
    on u.id = t.uketuke_id
INNER JOIN shouhins s
    on t.id = s.taisho_id
WHERE
    u.id = 1

モデルにアソシエーションを記載する

こんな風にモデルにアソシエーションを定義しておくと、テーブル同士の結合も楽チンにかけます。

#uketuke.rb

#受付は多の対象者を持っている
has_many :taisho
#受付は対象を通して、多の商品を持っている
has_many :shohin,through: :shohins


#taisho.rb

#対象者は多の商品を持っている
has_many :shohin
#対象者は受付情報に属している(1対多で)
belongs_to :uketuke


#shohin.rb

#商品は対象者に属している
belongs_to :taisho
#商品は対象者を通して、受付情報に属している
belongs_to :uketuke,through: :taisho

で、コントローラーでこう。

#コントローラー

Uketuke.joins(:shohin).select('欲しい列名').where('uketukes.id = 1')

これで上のSQLが1行でかけますし、3つのテーブルから好きな列を取り出せます。

さらに、例えば次のようなSQLがあったとします。

Joinするテーブルが増えても楽チン


SELECT u.*,t.*,s.*,user.user_name,st.shohin_name
FROM uketukes u
INNER JOIN taishos t
    on u.id = t.uketuke_id
INNER JOIN shouhins s
    on t.id = s.taisho_id
--増えた部分
--userのidだけでなく、氏名を別のテーブルから、
--商品のidだけでなく、商品の名前を別のテーブルから持ってきたい
INNER JOIN users user
    on u.created_user = user.id
INNER JOIN shohin_tables st
    on s.shohin_id = st.id
--
WHERE
    u.id = 1

↑こんなsqlもアソシエーションを貼っておけば、 1行で実現できます。

#uketukes.rb
has_many :createdUser,primary_key: :created_user,foreign_key: :id,class_name :User
#↑usersテーブルとuketukesのcreated_userというカラムとusersのidを利用して結合する
has_many :shohinTable,through: :shouhins
#↑shouhinsテーブルを通して、shouhinTableを多数持つ

#users.rb
has_many :uketukes,foreign_key: :created_user

#shouhins.rb
has_many: :shohinTable,primary_key: :shohin_id,foreign_key: :id

#shohin_tables.rb(商品の名前とかそういうのが記載されたテーブルのイメージ。紛らわしすぎてダメ。)
has_many: :shouhins,foreign_key: :shohin_id 
has_many: :uketukes,through: :shouhins

で、コントローラーはこんな感じです。

Uketuke.joins(:shohinTable).joins(:createdUser).select('uketukes.*,taishos.*,shohins.*,users.user_name,shohin_tables.shohin_name')

このように複数のアソシエーションを記載しておくことで、長ーいsql文をたった、1行で書けますし、where句で条件を絞るのも楽チンです。

↓例


#paramsでユーザーが動的に条件を指定してくる

where_sql = "  0 = 0  "
where_sql += " AND users.id = #{params[:created_user_id]} "    if params[:created_user_id].present?

Uketuke.joins(:shohinTable).joins(:createdUser).select('uketukes.*,taishos.*,shohins.*,users.user_name,shohin_tables.shohin_name').where(where_sql)

まとめ

  • アソシエーションを定義しておくと、複数行に渡るテーブルの結合なども簡単に再現できる
    • 流したいsqlを非常に短いコードで実装できるし、テーブル定義の変更時も変更する箇所が少ない
  • 内部結合以外にも、left joinなども書ける。