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. まとめ
まず、個人的な意見として、それぞれの方法のメリット・デメリットをまとめました。メリット | デメリット | |
---|---|---|
ActiveRecord | ・1, 2テーブル内での検索なら簡単に処理をかけれる ・返り値がActiveRecord::Relationやモデルのインスタンスなので扱いやすい | ・複数のテーブルにまたがる検索で、検索条件、他テーブルのカラム値の扱いがしずらい ・CASE WHENやカラムなど行いづらい |
Arel | 文字列でなくメソッドで検索文を記載できる(個人的にあまりメリットが見えていない) | RailsのプライベートAPIなので使うべきではない。参考 |
生SQL | 複雑なSQLをかける | 返り値がHashなのでモデルのメソッドが使いずらい |
メインでよく行う処理では比較的簡単なSQL(ActiveRecordで十分足りる)で処理をおこなえ、レポート集計などは複雑なSQL(生のSQL)でもしょうがないと思います。
こういう状態が、保守性、パフォーマンス、メンテナンス性などに優れた良いDB設計ができているということだと個人的に思います。
つまり、メイン処理のSQLが複雑になってしまうということは、どこかでDB設計のミスを犯していたり、パフォーマンスのために正規化を崩しすぎてしまったのではと思います。
2. ER図
ER図
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"]]
以上です。