Rails3 - Active Record Query Interface
- 準備
- オブジェクトをひとつだけ取り出す
- 複数のオブジェクトを取り出す
- 複数のオブジェクトをまとめて処理する
- ActiveRecord::Relation
- Where
- Order
- Select
- Limit, Offset
- Group
- Having
- 上書き - Overriding
- 読み込み専用 - Readonly
- ロック - Locking
- Join
ActiveRecordのクエリーインタフェースの解説 を参考に、コードを書いて確認していきます。
準備
bundlerでインストールします。
# Gemfile
source 'https://rubygems.org'
gem "activerecord", "~> 3.2.12"
gem 'sqlite3'
active_recordを単体で使えるようにセットアップします。
# main.rb
require 'rubygems'
require 'bundler/setup'
require "active_record"
# database connection
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:"
)
例で使用するマイグレーションとモデルを作成します。
# main.rb
# migration
class Init < ActiveRecord::Migration
def self.up
create_table(:clients){|t|
t.string :name
t.integer :orders_count
t.timestamps
}
create_table(:orders){|t|
t.references :client
t.integer :price
t.datetime :ordered_date
t.timestamps
}
create_table(:addresses){|t|
t.references :client
t.string :pref
}
end
def self.down
drop_table :clients
drop_table :orders
drop_table :addresses
end
end
# run migrate
Init.migrate(:up)
# models
class Client < ActiveRecord::Base
has_one :address
has_many :orders
end
class Address < ActiveRecord::Base
belongs_to :client
end
class Order < ActiveRecord::Base
belongs_to :client, :counter_cache => true
end
準備が整いました。データを入れてみます。
# main.rb
Client.create({:name => "Alice"})
Client.create({:name => "Bob"})
Client.create({:name => "Carol"})
Address.create({:client => Client.find(1), :pref => "Osaka"})
Order.create({:client => Client.find(2), :price=> 20})
Order.create({:client => Client.find(2), :price=> 50})
SQLを確認するためにloggerを設定します。
# main.rb
ActiveSupport::LogSubscriber.colorize_logging = false
ActiveRecord::Base.logger = Logger.new(STDOUT)
オブジェクトをひとつだけ取り出す
find - 主キーによる検索
普通は id で検索します。
# find
puts Client.find(1).name
# => SELECT "clients".* FROM "clients" WHERE "clients"."id" = ? LIMIT 1 [["id", 1]]
# "Alice"
もしデータが見つからなかった場合はActiveRecord::RecordNotFound
例外が発生します。
# find but not found
begin
Client.find(100)
rescue ActiveRecord::RecordNotFound => e
p e
#<ActiveRecord::RecordNotFound: Couldn't find Client with id=100>
end
以前はこの find の引数に色々パラメータを入れて複雑な条件をするのが主流でした。現在では後述のActiveRecord::Relation
を使う方が良いです。 find はもっぱら、主キーからオブジェクトを取得する目的だけに使われるようになったようです。
first
最初の項目を取得します。LIMIT 1
と同じです。
first
はレコードが存在していなければnil
を返しますが、first!
にすると、ActiveRecord::RecordNotFound
例外が発生します。
# first
puts Client.first.name
# => SELECT "clients".* FROM "clients" LIMIT 1
# "Alice"
last
最後の項目を取得します。降順にしてLIMIT 1
にすることで取得しています。
last
はレコードが存在していなければnil
を返しますが、last!
にすると、ActiveRecord::RecordNotFound
例外が発生します。
# last
puts Client.last.name
# => SELECT "clients".* FROM "clients" ORDER BY "clients"."id" DESC LIMIT 1
# "Carol"
複数のオブジェクトを取り出す
id の配列をfindすると一度に複数のオブジェクトを取得できます。
# find(array)
p Client.find([1,2])
# => SELECT "clients".* FROM "clients" WHERE "clients"."id" IN (1, 2)
# [#<Client id: 1, name: "Alice">, #<Client id: 2, name: "Bob">]
ひとつでもレコードが見つからなければやはりActiveRecord::RecordNotFound
です。
find, first, last は ActiveRecord::FinderMethods で定義されています。
複数のオブジェクトをまとめて処理する
テーブル内のレコード全件を処理したい場合はall
メソッドを使うことができます。
# all
Client.all.each do |c|
# ...
end
ただ、all
はテーブルの全データを取得して、インスタンス化し、メモリ内に保持するので、何千件もあるとすぐメモリ不足になります。
(allは実行された時点で、データベースにクエリーを投げます。戻り値はActiveRecord::Relation
ではありません。ここは間違いやすいので注意です)
この問題を解決するために、find_each
とfind_in_batches
という2通りの方法が用意されています。
find_each
全件をいくつかのブロックに分けて処理していくので、効率的に全件処理できます。。デフォルトでは1000件ごとです。
findの標準的なオプション(:order
, :limit
を除く)が使用できます。
# find_each
Client.find_each(:include => :address) do |c|
p c
end
# SELECT "clients".* FROM "clients" WHERE ("clients"."id" >= 0) ORDER BY "clients"."id" ASC LIMIT 1000
# SELECT "addresses".* FROM "addresses" WHERE "addresses"."client_id" IN (1, 2, 3)
#<Address id: 1, client_id: 1, pref: "Osaka">
# nil
# nil
# :start, :batch_size オプションが追加で使用できる
Client.find_each(:start => 2000, :batch_size => 5000) do |c|
# ...
end
:start
はバッチを再開する場合や、並列してワーカーを実行する場合などに利用できます。
find_in_batches
find_eachと似てますが、こちらはブロックの引数が配列になります。
# find_in_batches
Client.find_in_batches(:include => :orders, :batch_size => 2) do |clients|
puts clients.size
end
# 一回目のバッチ
# SELECT "clients".* FROM "clients" WHERE ("clients"."id" >= 0) ORDER BY "clients"."id" ASC LIMIT 2
# SELECT "addresses".* FROM "addresses" WHERE "addresses"."client_id" IN (1, 2)
# => 2
# 二回目のバッチ
# SELECT "clients".* FROM "clients" WHERE ("clients"."id" > 2) ORDER BY "clients"."id" ASC LIMIT 2
# SELECT "addresses".* FROM "addresses" WHERE "addresses"."client_id" IN (3)
# => 1
ActiveRecord::Relation
条件を指定して取得するには以下のメソッドを使います。これらはActiveRecord::Relation
オブジェクトを返します。
- where
- select
- group
- order
- reorder
- reverse_order
- limit
- offset
- joins
- includes
- lock
- readonly
- from
- having
p Client.where("1").class
# ActiveRecord::Relation
find
に同じようなオプションを与えることもできます(古いやり方)。
Where
SQLのWhere文を構築します。String, Array, Hash のどれかを引数に入れて使います。
Stringの場合、SQLを直接書くようなイメージです。エスケープなどはされません。ユーザー入力値をそのまま入れないようにしましょう。
# where(string)
Client.where("orders_count = '2'")
# SELECT "clients".* FROM "clients" WHERE (orders_count = '2')
# => [#<Client id: 2, name: "Bob", orders_count: 2, created_at: "2013-03-06 19:12:44", updated_at: "2013-03-06 19:12:44">]
# これはやってはいけない!
Client.where("first_name LIKE '%#{params[:first_name]}%'")
Arrayにすると、プレースホルダーが使えます。?
にエスケープされた値が入るので安全です。
詳細。
# where(array, ...)
Client.where("orders_count = ?", params[:orders])
プレースホルダーはハッシュにもできます。
# where(array, hash)
Client.where("created_at >= :start_date AND created_at <= :end_date",
{:start_date => params[:start_date], :end_date => params[:end_date]})
Hashにするとすっきりします。
# where(hash)
Client.where(:orders_count => 2)
範囲指定もできます。
# where(key => range)
Client.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight)
# sql:
# SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
Subset条件(IN構文)を使うには、値を配列にします
# where(key => array)
Client.where(:orders_count => [1,3,5])
# sql:
# SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
Order
SQL の ORDER BY そのままです。
# order by
Client.order("created_at")
Client.order("orders_count ASC, created_at DESC")
Select
SELECT句です。これを指定すると、取得されたオブジェクトは Readonly になります。
# select
Client.select(:orders_count)
# sql:
# SELECT orders_count FROM "clients"
selectとした列以外を取得しようとすると、ActiveRecord::MissingAttributeError になります。
# select error
c = Client.select(:orders_count).first
begin
p c.name
rescue ActiveModel::MissingAttributeError => e
p e
end
# #>ActiveModel::MissingAttributeError: missing attribute: name
SELECT DISTINCT
相当は uniq()
です。
# select distinct
Client.select(:name).uniq
# sql:
# SELECT DISTINCT name FROM clients
q = Client.select(:name).uniq
q = uniq(false) # uniq解除
Limit, Offset
LIMIT/OFFSET句です。
# limit, offset
Client.limit(5)
Client.offset(30)
Group
# group by
Client.group("date(created_at)")
# sql: SELECT "clients".* FROM "clients" GROUP BY date(created_at)
Having
# having
Order.select("date(created_at) as ordered_date, sum(price) as total_price")
.group("date(created_at)")
.having("sum(price) > ?", 100)
# sql: SELECT date(created_at) as ordered_date, sum(price) as total_price FROM "orders" GROUP BY date(created_at) HAVING sum(price) > 100
上書き - Overriding
構築したクエリから一部を除外したり、特定の条件だけにするメソッドが用意されてます。
except()
: 指定クエリを除外(:order
,:where
など)only()
: 指定クエリだけにする(:order
,:where
など)reorder()
: default scope で指定した order を上書きしますreverse_order()
: 逆順にします
except
を使ってみます。例外的な処理が発生する場合に使えるかもです。
# except
clients = Client.where("orders_count > 0")
p clients
# [#<Client id: 2, name: "Bob">]
clients = clients.except(:where)
p clients
# [#<Client id: 1, name: "Alice">, #<Client id: 2, name: "Bob">, #<Client id: 3, name: "Carol">]
読み込み専用 - Readonly
Readonlyなオブジェクトを更新しようとすると、例外が発生します。
# readonly
client = Client.readonly.first
client.name = "hoge"
client.save # raise ActiveRecord::ReadOnlyRecord
ロック - Locking
楽観的ロック
integer型のlock_version
という名前のカラムが存在すると、Railsが自動的に楽観的ロックを行なってくれます。
更新する度にlock_version
の値をインクリメントしていき、競合を検知する仕組みです。
更新が競合した場合、ActiveRecord::StaleObjectError
が発生します。
# lock_version
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
# sql:
# begin transaction
# UPDATE "clients" SET "name" = 'Michael', "updated_at" = '2013-03-13 17:17:59.886521', "lock_version" = 1 WHERE ("clients"."id" = 1 AND "clients"."lock_version" = 0)
# commit transaction
c2.name = "should fail"
c2.save # Raises an ActiveRecord::StaleObjectError
ActiveRecord::Base.lock_optimistically = false
でこの機能を無効にできます。
set_locking_column
で既定のlock_version
というカラム名を変更できます。
# set_locking_column
class Client < ActiveRecord::Base
set_locking_column :lock_client_column
end
悲観的ロック
悲観的ロックはデータベースシステムの機能を利用します。
リレーションを構築する時にlock
を使うと、選択行の排他的ロックを獲得します。
lock
を使ったリレーションはデッドロックを避けるために、通常transaction
ブロックで囲みます。
# transaction
Address.transaction do
a = Address.lock.first
a.pref = "Hokkaido"
a.save
end
# begin transaction
# UPDATE "addresses" SET "pref" = 'Hokkaido' WHERE "addresses"."id" = 1
# commit transaction
共有ロックなどロックタイプを変更したい場合は引数にタイプを与えてやります。
# lock in share mode
Address.transaction do
a = Address.lock("LOCK IN SHARE MODE").first
a.increment!(:views)
end
すでにインスタンスを取得しているなら、with_lock
でトランザクションを開始できます。
# with_lock
a = Address.first
a.with_lock do
# このブロックはtransaction内で、itemはロックされてます
a.increment!(:views)
end
Join
TODO