RailsでActiveRecord/Arelを使って複雑なSELECT文を実行する方法

http://ruby.studio-kingdom.com/rails/guides/association_basics

Rails(ActiveRecord/Arel)で複雑なSELECT文を実行する方法をまとめました。
メリット、デメリットや使いどころなどまとめ途中なのであしからずお願いします。

動作確認

  • Ruby 2.2.0
  • Rails 4.2.0
  • Arel 6.0.0

目次

  1. まとめ
  2. ER図
  3. Active Record
  4. Arel
  5. 生SQL

1. まとめ

まず、個人的な意見として、それぞれの方法のメリット・デメリットをまとめました。

  メリット デメリット
ActiveRecord
 ・1, 2テーブル内での検索なら簡単に処理をかけれる
 ・返り値がActiveRecord::Relationやモデルのインスタンスなので扱いやすい

 ・複数のテーブルにまたがる検索で、検索条件、他テーブルのカラム値の扱いがしずらい
 ・CASE WHENやカラムなど行いづらい
Arel文字列でなくメソッドで検索文を記載できる(個人的にあまりメリットが見えていない)RailsのプライベートAPIなので使うべきではない。参考
生SQL複雑なSQLをかける返り値がHashなのでモデルのメソッドが使いずらい


メインでよく行う処理では比較的簡単なSQL(ActiveRecordで十分足りる)で処理をおこなえ、レポート集計などは複雑なSQL(生のSQL)でもしょうがないと思います。
こういう状態が、保守性、パフォーマンス、メンテナンス性などに優れた良いDB設計ができているということだと個人的に思います。
つまり、メイン処理のSQLが複雑になってしまうということは、どこかでDB設計のミスを犯していたり、パフォーマンスのために正規化を崩しすぎてしまったのではと思います。



2. ER図

ER図

f:id:nipe880324:20150428015849j:plain

DBスキーマとモデルの作成

bin/rails g model User username email password
bin/rails g model Order order_no:integer user_id:integer
bin/rails g model LineItem quantity:integer book_id:integer
bin/rails g model Book isbn name name_kana price:integer
bin/rails g model Category
bin/rake db:migrate

モデルの定義

class User < ActiveRecord::Base
  has_many :orders
end

class Order < ActiveRecord::Base
  belongs_to :user
  has_many :line_items

  def total_price
    line_items.map(&:total_price).sum
  end
end

class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :book

  def total_price
    book.price * quantity
  end
end

class Book < ActiveRecord::Base
  has_many :line_items
  belongs_to :category
end

class Category < ActiveRecord::Base
  has_many :books
end

3. Active Record

簡単にテーブルからデータを取得でき、また、オブジェクトとして取得できるので、データを扱いやすいです。
しかし、複雑なSQLを実行する場合は、ActiveRecordにより作成されるSQLを確認しないと、パフォーマンスが悪いSQLになっている可能性もあるので注意が必要です。

joinsメソッドを使うことで次のようにINNER JOINをして、複数のテーブルに対してWHERE句を実施することができます。

users = User.joins(orders: { line_items: { book: :category }}).where(users: { username: 'test' }, categories: { name: 'IT' })

SELECT "users".* FROM "users"
INNER JOIN "orders" ON "orders"."user_id" = "users"."id"
INNER JOIN "line_items" ON "line_items"."order_id" = "orders"."id"
INNER JOIN "books" ON "books"."id" = "line_items"."book_id"
INNER JOIN "categories" ON "categories"."id" = "books"."category_id"
WHERE "users"."username" = 'test' AND "categories"."name" = 'IT'

includesメソッドを使うことで次のようにLEFT OUTTER JOINをして、複数のテーブルに対してWHERE句を実施することができます。

users = User.includes(orders: { line_items: { book: :category }}).where(users: { username: 'test' }, categories: { name: 'IT' })

SELECT "users"."id" AS t0_r0, "users"."username" AS t0_r1, "users"."email" AS t0_r2, "users"."password" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "orders"."id" AS t1_r0, "orders"."order_no" AS t1_r1, "orders"."user_id" AS t1_r2, "orders"."created_at" AS t1_r3, "orders"."updated_at" AS t1_r4, "line_items"."id" AS t2_r0, "line_items"."quantity" AS t2_r1, "line_items"."book_id" AS t2_r2, "line_items"."order_id" AS t2_r3, "line_items"."created_at" AS t2_r4, "line_items"."updated_at" AS t2_r5, "books"."id" AS t3_r0, "books"."isbn" AS t3_r1, "books"."name" AS t3_r2, "books"."name_kana" AS t3_r3, "books"."price" AS t3_r4, "books"."category_id" AS t3_r5, "books"."created_at" AS t3_r6, "books"."updated_at" AS t3_r7, "categories"."id" AS t4_r0, "categories"."name" AS t4_r1, "categories"."created_at" AS t4_r2, "categories"."updated_at" AS t4_r3
FROM "users"
LEFT OUTER JOIN "orders" ON "orders"."user_id" = "users"."id"
LEFT OUTER JOIN "line_items" ON "line_items"."order_id" = "orders"."id"
LEFT OUTER JOIN "books" ON "books"."id" = "line_items"."book_id"
LEFT OUTER JOIN "categories" ON "categories"."id" = "books"."category_id"
WHERE "users"."username" = 'test' AND "categories"."name" = 'IT'

また、オブジェクトとして帰ってくるので、その後に処理を行いやすいです。

user = User.includes(orders: { line_items: { book: :category }}).where(users: { username: 'test' }).first
user.orders.count

扱いづらいが、selectメソッドにより他テーブルの値を取得し、アクセスできます。

user = User.select('users.*, orders.order_no AS order_no').joins(:orders).find(1)
#=> SELECT  users.*, orders.order_no AS order_no FROM "users" INNER JOIN "orders" ON "orders"."user_id" = "users"."id" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]

# selectで指定した値を取得できます
user.attributes
#=> {"id"=>1, "username"=>"test", "email"=>"hoge@example.com", "password"=>nil, "created_at"=>Mon, 27 Apr 2015 15:49:16 UTC +00:00, "updated_at"=>Mon, 27 Apr 2015 15:49:16 UTC +00:00, "order_no"=>1}

# もちろんアクセスもできます
user.order_no #=> 1

4. Arel

Arel - GitHub公式によると、複雑なSQLクエリの生成を簡単にするらしいです。(個人的にあまりメリットが見えていません)
また、一説によると、RailsのプライベートAPIなので使うべきではないらしいです。参考

users  = User.arel_table
orders = Order.arel_table
sql    = users.join(orders, Arel::Nodes::OuterJoin).on(users[:id].eq(orders[:user_id])).where(orders[:order_no].eq('1')).to_sql
#=> "SELECT FROM \"users\" LEFT OUTER JOIN \"orders\" ON \"users\".\"id\" = \"orders\".\"user_id\" WHERE \"orders\".\"order_no\" = 1"

5. 生SQL

副問い合わせ、CASE WHENなど複雑なSQLを記載でき、基本的にはSQLの知識はあると思うので学習コストは少ないと思います。
しかし、返り値がHashなのでモデルのメソッドが使いずらく、また、SQLサーバーに依存したコードになります。

# 実行したいSQLをヒアドキュメトで記載する
query = <<-SQL
  SELECT users.*
  FROM "users"
  LEFT OUTER JOIN "orders" ON "orders"."user_id" = "users"."id"
  LEFT OUTER JOIN "line_items" ON "line_items"."order_id" = "orders"."id"
  LEFT OUTER JOIN "books" ON "books"."id" = "line_items"."book_id"
  LEFT OUTER JOIN "categories" ON "categories"."id" = "books"."category_id"
  WHERE "users"."username" = 'test'
SQL

# SQLを実行する
hash = ActiveRecord::Base.connection.select_all(query)
#=> #<ActiveRecord::Result:0x007f9ab9869c90 @columns=["id", "username", "email", "password", "created_at", "updated_at"], @rows=[[1, "test", "hoge@example.com", nil, "2015-04-27 15:49:16.971518", "2015-04-27 15:49:16.971518"]], @hash_rows=nil, @column_types={}>

# 表形式で取得できる
hash.columns
#=> ["id", "username", "email", "password", "created_at", "updated_at"]
hash.rows
#=> [[1, "test", "hoge@example.com", nil, "2015-04-27 15:49:16.971518", "2015-04-27 15:49:16.971518"]]


以上です。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值