LoginSignup
84
81

More than 5 years have passed since last update.

ActiveRecordでサブクエリ+NOT EXISTSを発行

Last updated at Posted at 2015-02-11

概要

ActiveRecordを利用したコーディングにおいて、NOT EXISTSINNER JOINを組み合わせた、少し複雑なクエリが必要となった。

そこで、Arelを使用した、生のSQLを書かない方法で実装してみた。

Arelとは

第43回 Rails 3を支える名脇役たち その1 - Arel -

Arelとは,「Relational Algebra」または「Active Relation」の略で,その名前から想像がつくとおり,ActiveRecordから派生した,「関係代数」をRubyのオブジェクトで取り扱うためのライブラリです。

ドキュメントによれば,Arelは,

  1. 面倒なSQLの生成を簡単にしてくれて,
  2. さまざまなデータベースシステムに対応している,

「フレームワークのフレームワーク」を目指して作られています。

つまり,Arelを使えば,DBの互換性やSQL文字列の生成などに惑わされることなく,大事な設計やモデリングに注力してO/Rマッピング処理を実装することができるようになっています。

とても簡単に要約すると、

  • SQLを簡単に生成してくれる
  • 様々なRDBMSに対応してくれる

ということだと思います。

テーブル概要

超簡易化したテーブル概要は以下のとおり。

テーブル名 概要
members メンバーー覧。IDと名前を持つ。
events イベント一覧。IDと名前を持つ。
groups グループ一覧。イベントごとのグループを保持。IDと名前、加えてイベントIDを持つ。
groups_members グループとメンバー間の関連。グループIDとメンバーIDを持つ。

目的

とあるイベント(id: x)に参加していないメンバーを抽出したい。

そのためには、

  1. groupsをイベントIDで絞り、イベントに関連するグループを抽出する
  2. groupsgroups_membersJOINし、イベント参加者一覧を抽出する
  3. イベント参加者一覧に存在しないメンバーを抽出する

というクエリを作成すれば良い。

SQLによる記述

具体的には、以下のようなクエリを生成したい。

SELECT
    *
  FROM
    members a
  WHERE
    NOT EXISTS (
      SELECT
          *
        FROM
          groups b JOIN groups_members c
            ON b.id = c.group_id
          AND b.event_id = x -- 任意のイベントID
        WHERE
          a.id = c.member_id
    )
;

Arelによる記述

Arel::Tableprojectjoinonwhereを組み合わせて、上記のSQLを再現してみた。

members = Member.arel_table
groups = Group.arel_table
groups_members = GroupsMember.arel_table

condition =
  groups
    .project(Arel.star)
    .join(groups_members)
    .on(groups[:id].eq(groups_members[:group_id]), groups[:event_id].eq(x))
    .where(members[:id].eq(groups_members[:member_id]))

Member.where(condition.exists.not).all

ここで出てきたprojectという馴染みのないワードは、projectionの略だそうです。

Weblio - projectionによると、「射影」という意味があります。射影は属性の抽出をする演算で、SQLでいうとSELECTにあたります。つまり、projcet(Arel.star)SELECT *のことです。

詳細はWikipedia大先生に任せます。

生成されるSQL

上記のコードから生成されたSQLは以下の通りとなりました。

SELECT
    "members" . *
  FROM
    "members"
  WHERE
    (
      NOT (
        EXISTS (
          SELECT
              *
            FROM
              "groups" INNER JOIN "groups_members"
                ON "groups" . "id" = "groups_members" . "group_id"
              AND "groups" . "event_id" = x
            WHERE
              "members" . "id" = "groups_members" . "member_id"
        )
      )
    )
  ORDER BY
    id
;

無事、目的のSQLとなっていました。

条件の追加

メンバーがアクティブのものだけを抽出する必要があることに後から気がついたので、以下のようにwhereを追加してみました。

Member.where(condition.exists.not).where(active: true).all

Tips

condition.to_sqlのようにすると、具体的なSQLが生成されます。Rails consoleを使って適宜to_sqlをし、生成されるSQLと睨めっこしながら作業を進めました。

参考文献

以下の記事を参考にしました。詳細な解説は以下の記事の方が充実しています。

謝辞

@jnchito さんに、Arelのコード中にあったミスを修正していただきました。
ありがとうございます。

84
81
4

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
84
81