🛤

Rails で UNION ALL

に公開

Rails をちゃんと使い始めて4年になるんだけど、未だに Rails というか ActiveRecord がよくわかってない。

この前 UNION ALL を使う必要があってググったり AI に聞いたりてみたんだけど、別な gem が必要とか、Arel で書けとかばかりで、ActiveRecord イマイチだなーと思ったんだけど、ちゃんと API を見てみたら with を使えば UNION ALL が使えることを知った。

https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-with

たとえば、こんなふうに書くと、

User.with(hoge: [User.where(code:'0003'), User.where(code:'0008')])
.from("hoge").select(:id, :code).to_a

こんなクエリが生成される。

WITH "hoge" AS (
  SELECT "users".* FROM "users" WHERE "users"."code" = '0003'
  UNION ALL
  SELECT "users".* FROM "users" WHERE "users"."code" = '0008'
)
SELECT "id", "code" FROM hoge

戻り値は User オブジェクトの配列。

[#<User:0x00007a99da785160 id: 3, code: "0003">,
 #<User:0x00007a99da785020 id: 8, code: "0008">]

便利。

次のような異なるテーブルを UNION で連結したい場合でも

WITH hoge AS (
  SELECT code,'order' as type FROM orders WHERE code = '0003'
  UNION ALL
  SELECT code,'inventory' as type FROM invnetries WHERE code = '0003'
)
SELECT code, type FROM hoge

こんな風に書くと、

order = Order.where(code: '0003').select(:code, "'order' as type")
inventory = Inventory.where(code: '0003').select(:code, "'inventory' as type")
Order.with(hoge: [order, inventory])
.from("hoge").select(:code, :type).to_a

こんなクエリが生成される。

WITH "hoge" AS (
  SELECT "orders"."code", 'order' as type FROM "orders" WHERE "orders"."code" = '0003'
  UNION ALL
  SELECT "inventories"."code", 'inventory' as type FROM "inventories" WHERE "inventories"."code" = '0003'
)
SELECT "code", "type" FROM hoge

戻り値は Order オブジェクトの配列。

[#<Order:0x00007bab3bca6180 code: "0003", type: "order", id: nil>,
 #<Order:0x00007bab3bca6040 code: "0003", type: "inventory", id: nil>]

けど、結果は Order とは全然関係ない構造なのに、Order オブジェクトになるのが気持ち悪い。
with の左に何か書かないといけないというのがイマイチ。

with の中に指定したモデルクラス(Order, Inventory)とは全然関係ないクラスでもいい。
ホントになんでもいいので、次のようにも書ける。

User.with(hoge: [order, inventory])
.from("hoge").select(:code, :type).to_a

これでも生成されるクエリは同じ。結果のオブジェクトが User になるのが異なるだけ。

[#<User:0x00007bab3bae8c80 code: "0003", type: "order", id: nil>,
 #<User:0x00007bab3c7c3c20 code: "0003", type: "inventory", id: nil>]

この手のクエリを発行したいときはどうせ結果のクラスには興味はないので、pluck を使って

Order.with(hoge: [order, inventory])
.from("hoge").pluck(:code, :type)
#=> [["0003", "order"], ["0003", "inventory"]]

みたいにしてもいいんだけど、それでも with の左に何か書かないといけない。気持ち悪い。

まあそもそも ActiveRecord はテーブル=クラス、レコード=オブジェクトという思想で作られているものなので、凝ったクエリを書くには向いてないので仕方ない。

外部から条件を指定されて where を組み立てることを考えたら生SQL を文字列で書くのもやりたくない。
個人的には Arel も使いたくない。

そこで Sequel ですよ。

Sequel は O/Rマッパーで、Active Record パターンのライブラリとしても使えるんだけど、クエリビルダとしても使えるので便利。
クエリビルダとして使う場合は戻り値は単純なハッシュ。

DB = Sequel.postgres('dbname')
DB.from(:orders).where(code: '0003').to_a
#=>
# [{id: 3,
#   code: "0003",
#   name: "製品3",
#   created_at: 2025-11-30 07:58:51.34866 +0900,
#   updated_at: 2025-11-30 07:58:51.34866 +0900}]

UNION ALL はこんな感じで簡単。

order = DB.from(:orders).where(code: '0003').select(:code, Sequel.as('order', :type))
inventory = DB.from(:inventories).where(code: '0003').select(:code, Sequel.as('inventory', :type))
order.union(inventory, all: true).to_a
#=> [{code: "0003", type: "order"}, {code: "0003", type: "inventory"}]

Sequel で簡単にできるとは言ってもすでに ActiveRecord を使ってるアプリで Sequel を使うとなると、ActiveRecord 用の DB 接続とは別に Sequel 用の DB 接続が必要になるし、接続が異なるとトランザクション周りで変なことになるので、簡単に Sequel を使うというわけにはいかない。

…と思ってたんだけど、sequel-activerecord_connection という gem があって、これを使うと ActiveRecord 用の接続を Sequel から使えることを知った。

PostgreSQL なら、次のようにすると ActiveRecord の接続を使って Sequel が動く。便利。

DB = Sequel.postgres(extensions: :activerecord_connection)

トランザクションの状態もちゃんと共有される。

DB.in_transaction?  #=> false
ActiveRecord::Base.transaction do
  DB.in_transaction?  #=> true
end
DB.in_transaction?  #=> false

ActiveRecord はフォームの入力をバリデーションして保存したり、クエリ結果をビューに埋め込んで返すのにはすごい便利なんだけど、API モードの Rails で結果を JSON で返す場合とかはレコード=オブジェクトである必要はないので、Sequel でいいことも多そう。

Sequel の戻り値はただの Hash なので、大量のレコードを返すような場合は ActiveRecord よりも軽いんじゃないかな。しらんけど。

まとめ

  • ActiveRecord でも with を使えば UNION ALL できるよ
  • Sequel は便利だよ
  • sequel-activerecord_connection を使えば ActiveRecord と Sequel を共存できるよ

Discussion