Activeレコードのクエリーインターフェース
このガイドでは、Active Recordを使用してデータベースからデータを取得する別の方法について説明していきます。 このガイドを読むことで、次の事が学べるはずです。
- 様々なメソッドと条件でレコードを見つける方法
- 見つけたレコードの並び順、属性の取得、グルーピング、その他のプロパティを指定する方法
- データ取得のために必要なデータベースへのクエリー数を減らす"eager loading"の使用方法
- 動的なfind系メソッドの使用方法
- 特定のレコードの存在の確認方法
- Active Recordモデル上での様々な計算方法について
- 関連付け上での"EXPLAIN"の実行方法
データベースのレコードを見つけるために生のSQLを使用している場合、 同じ操作をRails上でより良い方法で実行する事が可能です。 Active Recordでは、多くのケースで直接のSQLの使用を分離しようとします。
このガイドでのコードの例では、下記のモデルを参照することとします。
これ以降の全てのモデルは、特に指定が無い限り、id
を主キーとして使用します。
class Client < ActiveRecord::Base
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
class Address < ActiveRecord::Base
belongs_to :client
end
class Order < ActiveRecord::Base
belongs_to :client, counter_cache: true
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :clients
end
Active Recordはデータベース上でクエリーの実行を行い、 また多くのデータベースとの互換性を持ちます。(MySQL、PostgreSQL、SQLite等) Active Recordメソッドは、データベースに依存することなく同じように使用することが出来ます。
- 1. データベースからオブジェクトを取得
- 2. 条件
- 3. 並び順
- 4. 特定のフィールドを選択
- 5. limit文とoffset文
- 6. group文
- 7. having文
- 8. 条件の上書き
- 9. Null関連付け
- 10. 読み込み専用オブジェクト
- 11. Updateでのレコードのロック
- 12. テーブルの連結
- 13. Eager Loading連携
- 14. スコープ
- 15. 動的ファインダー
- 16. findまたはbuildによる新しいオブジェクト
- 17. SQL文によるfind
- 18. オブジェクトの存在
- 19. 計算
- 20. EXPLAINの実行
1. データベースからオブジェクトを取得
データベースからオブジェクトを取得するために、Active Recordはいくつかのfind系のメソッドを提供します。 各find系のメソッドは生のSQLを書くこと無く、受け取った引数からデータベース上で一定のクエリーを実行します。 メソッドには下記のものがあります。
bind
create_with
eager_load
extending
from
group
having
includes
joins
limit
lock
none
offset
order
preload
readonly
references
reorder
reverse_order
select
distinct
uniq
where
上記の全メソッドは、ActiveRecord::Relation
のインスタンスを返します。
Model.find(options)
の主な操作は、下記のように要約することが出来ます。
- 提供されたoptionsをSQLクエリーとして評価できるものに変換します。
- SQLクエリーを実行し、データベースから一致する結果を取得します。
- 各結果の行をモデルに対応したRubyオブジェクトとしてインスタンス化します。
-
もし指定されていれば、
after_find
コールバックを実行します。
1.1 単一オブジェクトの取得
Active Recordは、単一のオブジェクトを取得するのに、異なる5つの方法を提供します。
1.1.1 主キーを使用
Model.find(primary_key)
を使用して、指定した主キーに一致するオブジェクトを取得することが出来ます。
例えば、
# 主キー(id)が10であるClientを検索
client = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">
これは下記のSQLとして評価されます。
SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
Model.find(primary_key)
は、マッチするレコードが見つからなかった場合、
ActiveRecord::RecordNotFound
例外を発生させます。
1.1.2 take
Model.take
は、暗黙的に並び順の指定をすること無くレコードを取得します。
例えば、
client = Client.take
# => #<Client id: 1, first_name: "Lifo">
上記は次のSQLとして評価されます。
SELECT * FROM clients LIMIT 1
Model.take
は、レコードが見つからず、また例外が発生していなければ、
nil
を返します。
取得したレコードは、データベースエンジンによっては変更されていることがあるかもしれません。
1.1.3 first
Model.first
は、主キーで並び替えられた最初のレコードを見つけます。
例えば、
client = Client.first
# => #<Client id: 1, first_name: "Lifo">
上記は、次のSQLとして評価されます。
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
Model.first
は、マッチするレコードが見つからず、また例外が発生していなければnil
を返します。
1.1.4 last
Model.last
は、主キーで並び替えられた最後のレコードを見つけます。
例えば、
client = Client.last
# => #<Client id: 221, first_name: "Russel">
このSQLは次のSQLとして評価されます。
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
Model.last
は、一致するレコードが見つからず、また例外が発生していなければnil
を返します。
1.1.5 find_by
Model.find_by
は、条件に一致する最初のレコードを見つけます。
例えば、
Client.find_by first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">
Client.find_by first_name: 'Jon'
# => nil
これは次のように書いたものとして評価されます。
Client.where(first_name: 'Lifo').take
1.1.6 take!
Model.take!
は、暗黙的な並び替えを行わずにレコードを取得します。
例えば、
client = Client.take!
# => #<Client id: 1, first_name: "Lifo">
これは次のSQLとして評価されます。
SELECT * FROM clients LIMIT 1
Model.take!
は、マッチするレコードが見つからなければ、ActiveRecord::RecordNotFound
を発生させます。
1.1.7 first!
Model.first!
は主キーで並び替えられた最初のレコードを見つけます。
例えば、
client = Client.first!
# => #<Client id: 1, first_name: "Lifo">
これは次のSQLとして評価されます。
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
Model.first!
は、マッチするレコードが見つからなければ、ActiveRecord::RecordNotFound
を発生させます。
1.1.8 last!
Model.last!
は、主キーで並び替えられた最後のレコードを見つけます。
client = Client.last!
# => #<Client id: 221, first_name: "Russel">
これは次のSQLとして評価されます。
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
Model.last!
は、マッチするレコードが見つからなければ、ActiveRecord::RecordNotFound
を発生させます。
1.1.9 find_by!
Model.find_by!
は条件に一致する最初のレコードを見つけます。
マッチするレコードが見つからなければ、ActiveRecord::RecordNotFound
を発生させます。
例えば、
Client.find_by! first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">
Client.find_by! first_name: 'Jon'
# => ActiveRecord::RecordNotFound
これは次のように書いたものとして評価されます。
Client.where(first_name: 'Lifo').take!
1.2 複数のオブジェクトの取得
1.2.1 複数の主キーの使用
Model.find(array_of_primary_key)
は、主キーの配列を受け取り、
それにマッチするレコードを全て配列として返します。
例えば、
# 主キーが1と10のClientを見つけます
client = Client.find([1, 10]) # Or even Client.find(1, 10)
# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
これはつぎのSQLとして評価されます。
SELECT * FROM clients WHERE (clients.id IN (1,10))
Model.find(array_of_primary_key)は、提供された全ての主キーで一致するものが見つからなければ、
ActiveRecord::RecordNotFound
例外を発生させます。
1.2.2 take
Model.take(limit)
は、明確な並び順の指定無しに、
最初からlimit
件数分のレコードを取得します。
Client.take(2)
# => [#<Client id: 1, first_name: "Lifo">,
#<Client id: 2, first_name: "Raf">]
これは、次のSQLとして評価されます。
SELECT * FROM clients LIMIT 2
1.2.3 first
Model.first(limit)
は、主キーの昇順で並び替え、
最初からlimit
件数分のレコードを取得します。
Client.first(2)
# => [#<Client id: 1, first_name: "Lifo">,
#<Client id: 2, first_name: "Raf">]
これは次のSQLとして評価されます。
SELECT * FROM clients ORDER BY id ASC LIMIT 2
1.2.4 last
Model.last(limit)
は主キーの降順で並び替え、
最初からlimit
件数分のレコードを取得します。
Client.last(2)
# => [#<Client id: 10, first_name: "Ryan">,
#<Client id: 9, first_name: "John">]
これは次のSQLとして評価されます。
SELECT * FROM clients ORDER BY id DESC LIMIT 2
1.3 一括処理での複数オブジェクトの取得
多くのユーザーに向けたニュースレターの送信やデータのエクスポートをするようなケースで、 大きなレコードの集まりの繰り返し処理が必要になることがあります。
これをそのまま表すと、下記のようになるかもしれません。
#これはユーザーが数千行あるようなケースで、大変非効率です。
User.all.each do |user|
NewsLetter.weekly_deliver(user)
end
ただし、User.all.each
はActive Recordに1つのパスでテーブル全体をfetchするように指示し、
行ごとにモデルオブジェクトを作成し、メモリ内にその全体のモデルの配列を保持してしまうため、
この手法はテーブルサイズが増えるに従って現実的では無くなっていきます。
実際に、もし大量のレコードを持っていた場合、取得したデータのコレクションが利用可能なメモリ容量を超えてしまうかもしれません。
Railsは、この問題を解決するために一括処理内でレコードを分割することによってメモリに負担が掛からないようにする、2つのメソッドを提供します。
1つ目のメソッドはfind_each
で、レコードの一括処理を取得し、モデルとして個別blockで各レコードをyieldします。
2つ目のメソッドはfind_in_batches
で、レコードの一括処理を取得し、
モデルの配列としてブロック全体の一括処理をyieldします。
find_each
とfind_in_batches
は、大量のレコードの一括処理で同時にメモリが使用されないようにしています。
1000レコード分のループ処理であれば、通常のfindメソッドの方が適しています。
1.3.1 find_each
find_each
メソッドはレコードのまとまりを取得し、各レコードをモデルとして個別にブロックでyield処理します。
下記の例では、find_eachは1000レコード(現在のfind_eachとfind_in_batchesでのデフォルト値)を取得し、
各レコードをモデルとして個別にブロックでyield処理します。
この処理は全てのレコードが処理されるまで、繰り返し処理されます。
User.find_each do |user|
NewsLetter.weekly_deliver(user)
end
1.3.1.1 find_eachのオプション
find_each
メソッドは、find_eachが内部で使用するために予約している:order
と:limit
を除く、
通常のfindメソッドのオプションのほとんどを使用することが可能です。
また、:batch_size
と:start
の追加オプションが利用可能です。
もし、同じ処理キューを扱う複数のワーカーが必要であれば、それはもう1つの例になるでしょう。(翻訳に自信なし)
各ワーカー上のオプションで:start
を設定することで、
各ワーカーに1000レコード扱わせるようにする事が出来ます。
1.3.2 find_in_batches
find_in_batches
メソッドはfind_each
に似ており、どちらのメソッドもレコードのまとまりを取得します。
2つの違いはfind_in_batches
は、個別に処理する代わりにブロック内でモデルの配列として、まとまりをyieldします。
下記の例では、1度に1000通の送り状の配列と最後はその残りを、提供されたブロックにyieldします。
# 1度に1000通の送り状の配列をadd_invoicesに渡します
Invoice.find_in_batches(include: :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
:include
オプションは、モデル読み込み時に関連付ける名前の指定を可能にしてくれます。(翻訳に自信なし)
1.3.2.1 find_in_batchesのオプション
find_in_batches
メソッドはfind_each
と同様、
find_in_batches
の内部使用のために予約されている:order
と:limit
を除く、
:batch_size
と:start
オプション、通常のfindメソッドで使用されるほとんどのオプションを受け取る事が出来ます。
2. 条件
where
メソッドは条件を指定することでSQL分のWHERE句に反映され、返されるレコードを限定することを可能にします。
条件には、文字列、配列、ハッシュのいずれかを指定することが出来ます。
2.1 文字列のみによる条件
もし、条件を追加したいのであれば、Client.where("orders_count = '2'")
のように指定することが出来ます。
これは、orders_countフィールドの値が2である、全てのClient(顧客)データを取得します。
条件を文字列だけで条件を構築する事は、SQLインジェクションの脆弱性の原因になり得ます。
例えば、Client.where("first_name LIKE '%#{params[:first_name]}%'")
と指定するのは危険です。
次のセクションで、配列を使用したより良い条件指定の方法を参照してください。
2.2 配列による条件
数値に任意の引数として指定されるようなケースでは、どうすればよいでしょう? そのようなケースでは、次のような形式で値を指定することが可能です。
Client.where("orders_count = ?", params[:orders])
Active Recordは、1つ目の要素の条件文字列内の?
マークを、
それ以降の要素の値に置き換えます。
もし複数の条件を指定したいのであれば、下記のようにします。
Client.where("orders_count = ? AND locked = ?", params[:orders], false)
この例では、1つ目の?はparams[:orders]
に置き換えられ、
2つ目の?は、SQLのデータベースシステムに依存したSQLとして解釈出来るfalse
値に置き換えられます。
安全上の理由から、下のコードでは無く上のコードのように書くべきです。
Client.where("orders_count = ?", params[:orders]) #安全
Client.where("orders_count = ") #危険
直接、条件文字列に値を置くことは、同様にデータベースにその値を渡すことと同義です。 これは、悪意を持っているかもしれないユーザーから直接渡されたエスケープされていない値かもしれません。 もし、この事が知られてしまうと、それを利用してデータベースに対して任意の操作が出来てしまうため、 データベース全体が危険に晒されてしまいます。 条件文字列内に直接引数を置くことは、絶対にしないでください。
SQLインジェクションの危険性についての詳細は、 セキュリティガイドを参照してください。
2.2.1 プレースホルダによる条件
パラメータを?に置き換える形式は、配列の条件内でkey/valueのハッシュでも指定することが可能です。
Client.where("created_at >= :start_date AND created_at <= :end_date",
{start_date: params[:start_date], end_date: params[:end_date]})
多くの条件指定をする必要がある場合は、こちらの方がより分かり易くすることが出来ます。
2.3 ハッシュによる条件
またActive Recordでは、より条件文の可読性を向上してくれるハッシュでの受け渡しも可能です。 ハッシュでの条件指定であれば、指定したいフィールドをキーに指定したい条件を値にしたハッシュを渡します。
ハッシュ条件に指定可能なものは、等価、範囲、部分集合、検査のみです。
2.3.1 等価の条件
Client.where(locked: true)
フィールド名は、文字列にすることも可能です。
Client.where('locked' => true)
belongs_toの関連付けが行われているケースでは、Active Recordオブジェクトが値として使用されていれば、 関連付けのキーはモデルを指定するために使用する事が出来ます。 このメソッドは、同様にポリモーフィズムでの関連付けで動作します。
Post.where(author: author)
Author.joins(:posts).where(posts: {author: author})
この値(author)はシンボルではありません。
例えば、Client.where(status: :active)
とすることは出来ません。
2.3.2 範囲の条件
Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
これは、昨日作成されたClient(顧客)を、BETWEEN
のSQL文を使用して取得します。
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
これは、配列による条件指定を短い構文で表したものになります。
2.3.3 部分集合の条件
もし、IN
式を使用してレコードを探したい場合、
条件ハッシュに配列を渡すことで、これを行うことが出来ます。
Client.where(orders_count: [1,3,5])
このコードは下記のようなSQLを生成します。
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
2.4 NOT条件
NOT
のSQLクエリーは、where.not
で構築することが出来ます。
Post.where.not(author: author)
言い換えると、クエリーは引数無しのwhere
に呼び出され、
その後のチェインでwhere
の条件が渡されたnot
によって生成することが出来ます。
3. 並び順
指定した並び順でデータベースからレコードを取得するために、order
メソッドを使用することが出来ます。
例えば、テーブル内のcreated_at
フィールドを昇順で並び替えてレコードの集まりを取得したい場合は、下記のようにします。
Client.order(:created_at)
# OR
Client.order("created_at")
同様にASC
またはDESC
を次のように指定します。
Client.order(created_at: :desc)
# OR
Client.order(created_at: :asc)
# OR
Client.order("created_at DESC")
# OR
Client.order("created_at ASC")
また、複数のフィールドを指定した並び替えも可能です。
Client.order(orders_count: :asc, created_at: :desc)
# OR
Client.order(:orders_count, created_at: :desc)
# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")
もし、例えば異なるコンテキストで複数のorder
を使用したのであれば、
新しいorder
を追加することが出来ます。
Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
4. 特定のフィールドを選択
デフォルトでModel.find
は、select *
を使用し、結果として全てのフィールドを取得します。
このフィールドを任意に取得するには、select
メソッドを使用します。
例えば、下記はviewable_byとlockedのカラムの結果のみを取得します。
Client.select("viewable_by, locked")
このSQLクエリーは下記のようになります。
SELECT viewable_by, locked FROM clients
インスタンス化されるモデルオブジェクトも、選択したフィールドしか持たない事に注意してください。 もし、選択していないフィールドにアクセスしようとすると、下記のエラーが発生します。
ActiveModel::MissingAttributeError: missing attribute: <attribute>
<attribute>には、問い合わせようとしたフィールドが入ります。
id
は関連付けが行われていると、ActiveRecord::MissingAttributeError
が発生しない事に注意してください。
これは関連付けの関数プロパティでid
が必要となるためです。
もし、特定レコードの一意の値ごとに単一のレコードを取得したいのであれば、distinct
を使用することが出来ます。
Client.select(:name).distinct
これは次のようなSQLを生成します。
SELECT DISTINCT name FROM clients
また、その一意制約を取り除く事も可能です。
query = Client.select(:name).distinct
# => 一意の名前を返します
query.distinct(false)
# => 重複していたとしても、全ての名前も返します
5. limit文とoffset文
Model.find
でSQLにLIMIT
を適用するには、
relation上でlimit
とoffset
を使用します。
limit
を使用して取得するレコード数を指定し、
offset
を使用してスキップするレコード数を指定してレコード取得開始位置を調整します。
例えば、
Client.limit(5)
上記は最大で5件のClient(顧客)レコードを返します。
offset
の指定が無いため、テーブルの先頭から5件が返されます。
発行されるSQLは下記のようになります。
SELECT * FROM clients LIMIT 5
下記は、offset
を追加することで、
Client.limit(5).offset(30)
31番目から始まるClient(顧客)レコードを最大5件返します。 SQLは下記のようになります。
SELECT * FROM clients LIMIT 5 OFFSET 30
6. group文
GROUP BY
をSQLに適用するには、group
メソッドを使用します。
例えば、注文(Order)の作成日(created_at)を日付けのまとまりとして取得したい場合は、下記のように指定します。
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
そしてこれは、データベース上の注文(Order)の各日付け毎に、単一のOrder
オブジェクトを与えてくれます。
SQLは下記のようになります。
SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
7. having文
SQLはHAVING
を指定して、Group BY
のフィールドに条件を指定することが出来ます。
having
メソッドを追加することで、SQL文に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
これは1日の注文が$100ドル以上である、各日付け毎の単一のOrderオブジェクトを返します。
8. 条件の上書き
8.1 except
except
メソッドを使用して、確定している条件を除外することが出来ます。
例えば、
Post.where('id > 10').limit(20).order('id asc').except(:order)
このSQLは下記のように実行されます。
SELECT * FROM posts WHERE id > 10 LIMIT 20
8.2 unscope
except
メソッドは、関連付けが結合されている場合には動作しません。
例えば、
Post.comments.except(:order)
上記は、もしCommentでデフォルトのscopeが設定されていると、order(並び替え)の条件を保持したままになります。
関連付けが連結していたとしても、全ての並び順の条件を削除したい場合は、
下記のようにunscope
を使用してください。
Post.order('id DESC').limit(20).unscope(:order) = Post.limit(20)
Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all
下記のように、where
句にunscope
の指定を追加することも可能です。
Post.where(:id => 10).limit(1).unscope(where: :id, :limit).order('id DESC') = Post.order('id DESC')
8.3 only
only
メソッドを使用して、条件を上書きすることも可能です。
例えば、
Post.where('id > 10').limit(20).order('id desc').only(:order, :where)
このSQLは次のように実行されます。
SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
8.4 reorder
reorder
メソッドは、デフォルトのscopeのorderを上書きします。
例えば、
class Post < ActiveRecord::Base
..
..
has_many :comments, order: 'posted_at DESC'
end
Post.find(10).comments.reorder('name')
このSQLは次のように実行されます。
SELECT * FROM posts WHERE id = 10 ORDER BY name
reorder
の指定が無ければ、SQLは次のようになります。
SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
8.5 reverse_order
reverse_order
メソッドは、指定されると並び順を反転します。
Client.where("orders_count > 10").order(:name).reverse_order
このSQLは次のように実行されます。
SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
このメソッドは、引数を受け取りません。
9. Null関連付け
none
メソッドは、レコード無しでチェーン可能なRelationを返します。
返されるRelationにチェーンされて続く条件は、空のRelationの生成を続行します。(翻訳に自信なし)
これは、メソッドにチェイン可能な応答をメソッドにする必要がある場合や、結果が0件のscopeを返すかもしれないような場合に便利です。
Post.none # 空のRelationを返し、クエリを実行しません。
# 下記のvisible_postsメソッドは、Relationを返す事を期待されます。
@posts = current_user.visible_posts.where(name: params[:name])
def visible_posts
case role
when 'Country Manager'
Post.where(country: country)
when 'Reviewer'
Post.published
when 'Bad User'
Post.none # => この場合[]またはnilを返すと、呼び出し元を破壊してしまいます。
end
end
10. 読み込み専用オブジェクト
Active Recordは、Relation上でreadonly
メソッドを提供し、返されるオブジェクトの変更を明示的に禁じます。
readonlyとされたレコードの変更を試みると、ActiveRecord::ReadOnlyRecord
例外が発生します。
client = Client.readonly.first
client.visits += 1
client.save
上記のコードではclientはreadonlyオブジェクトとして設定されるため、
visitsの値を更新するためにclient.saveが呼び出された時に、ActiveRecord::ReadOnlyRecord
例外が発生します。
11. Updateでのレコードのロック
ロック(Lock)処理は、データベース内のレコードの更新で、膨大な更新を確実にする際に競合状態を防止するのに役立ちます。
Active Recordは2つのロック処理のメカニズムを提供します。
- 楽観的ロック
- 悲観的ロック
11.1 楽観的ロック
楽観的ロックは複数のユーザーが編集のために同じレコードへアクセスすることを許可し、
データの競合が極めて少ない事を前提にしています。
そのレコードが開かれたかどうかで、別のプロセスがレコードに変更を加えたかどうかをチェックすることによってこれを行います。
もしそれが起こると、ActiveRecord::StaleObjectError
例外がスローされ、更新は破棄されます。
楽観的ロックのカラム
楽観的ロックを使用するには、テーブルにinteger型のlock_version
というカラムが必要になります。
レコードが更新される度に、Active Recordはlock_version
カラムの値を増やします。
もし更新するリクエストのがlock_version
が、現在のデータベースのlock_version
の値より低い場合、
更新リクエストはActiveRecord::StaleObjectError
エラーで失敗します。
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # ActiveRecord::StaleObjectError発生
このため衝突が起こった場合には、例外のrescueし、ロールバック、マージ、そうでなければビジネスロジックでその衝突を解決する必要あります。
この振る舞いは、ActiveRecord::Base.lock_optimistically = false
を設定することによって、
オフにすることが出来ます。
lock_version
のカラム名を上書きするには、
ActiveRecord::Base
が提供するlocking_column
のクラス属性を次のように変更します。
class Client < ActiveRecord::Base
self.locking_column = :lock_client_column
end
11.2 悲観的ロック
悲観的ロックは、データベースに実装されている機能が提供するメカニズムを使用します。 Relationを構築するときに、ロックを使用すると、選択した行の排他ロックを取得します。 ロックを使用したRelationは、通常はデッドロック状態を防ぐために内部のトランザクションをラップします。
例えば、
Item.transaction do
i = Item.lock.first
i.name = 'Jones'
i.save
end
上記のセッションは、MySQLのバックエンドのために下記のSQLを生成します。
SQL (0.2ms) BEGIN
Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms) COMMIT
また、異なるタイプのロックを使用できるように、lock
メソッドに生のSQLを渡すことも出来ます。
例えば、MySQLはレコードをロックするけれど、他のクエリーの読み込みを未だ許可するLOCK IN SHARE MODE
という式を持ちます。
この式が指定するには、下記のようにlock
オプション(lockメソッド?)を渡します。
Item.transaction do
i = Item.lock("LOCK IN SHARE MODE").find(1)
i.increment!(:views)
end
もし既にモデルのインスタンスを持っている場合、 下記のコードのようにすることで、トランザクションを開始し、ロックを取得することが出来ます。
item = Item.first
item.with_lock do
# このブロックはトランザクション内で呼ばれ、
# itemはロックされます。
item.increment!(:views)
end
12. テーブルの連結
Active RecordはSQLにJOIN
句を指定するjoins
と呼ばれるfind系のメソッドを提供します。
joins
メソッドを使用する複数の方法が存在します。
12.1 SQL文字列を部分的に使用
単純にjoins
に、JOIN句の生のSQLを適用する事が出来ます。
Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
これは、結果として下記のSQLになります。
SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
12.2 名前付き関連付けの配列/ハッシュの使用
このメソッドは、INNER JOIN
のみで動作します。
Active Recordはjoins
を使用する際に、
モデル上で定義された関連付けの名前をショートカットとしてJOIN句の指定に使用出来るようにしてくれます。
下記のCategory、Post、Comments、Guestモデルが定義されている前提で考察してみましょう。
class Category < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ActiveRecord::Base
belongs_to :post
has_one :guest
end
class Guest < ActiveRecord::Base
belongs_to :comment
end
class Tag < ActiveRecord::Base
belongs_to :post
end
この後に続くコードでは、意図したINNER JOIN
が使用されたjoinクエリーが生成されます。
12.2.1 単一の関連付けでのJOIN
Category.joins(:posts)
これは下記のSQLを生成します。
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
または英語で、"return a Category object for all categories with posts"
(postsに紐付く全てのカテゴリーのCategoryオブジェクトを返します)となります。
複数のpostが同じカテゴリーを持つ場合、重複したカテゴリーが含まれることに注意してください。
もし、カテゴリーを重複させたくないのであれば、Category.joins(:posts).uniq
を使用してください。
12.2.2 複数の関連付けのJOIN
Post.joins(:category, :comments)
これは下記のSQLを生成します。
SELECT posts.* FROM posts
INNER JOIN categories ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
または英語で、"return all posts that have a category and at least one comment" (カテゴリーと少なくとも1つのコメントも持つ全てのpostを返します。)となります。 前述の繰り返しになりますが、複数のコメントを持つpostが複数回表示されることに注意してください。
12.2.3 入れ子の関連付けによるJOIN(1階層)
Post.joins(comments: :guest)
これは下記のSQLを生成します。
SELECT posts.* FROM posts
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
または英語で、"return all posts that have a comment made by a guest." (ゲストによるコメントを持つ全てのpostを返します)となります。
12.2.4 入れ子の関連付けによるJOIN(複数階層)
Category.joins(posts: [{comments: :guest}, :tags])
これは下記のSQLを生成します。
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.post_id = posts.id
12.3 条件指定によるテーブル連結
通常の配列と文字列条件を使用して、テーブル連結した上で条件を指定することが可能です。 ハッシュによる条件指定は、特別な文法により連結するテーブルを指定します。
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)
入れ子でハッシュ条件を指定する、より整った別の文法で書くことも出来ます。
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: {created_at: time_range})
これは、再びBETWEEN
のSQL式を使用して、
昨日作成された注文(order)を持つ顧客(client)を全て見つけ出します。
13. Eager Loading連携
Eager loading(意欲的な読み込み)は、Model.find
によって返されるオブジェクトの関連レコードを、
出来るだけ少ないクエリーで読み込むためのメカニズムです。
N + 1クエリー問題
下記のコードで考えてみましょう。 これは顧客を10人見つけ、彼らの郵便番号を出力します。
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
このコードは、一見問題無いように見えますが、件数分のクエリーが実行されてしまうという問題を抱えています。 上記のコードは合計で、1(顧客10人を見つけるため) + 10(それぞれの顧客毎に住所を読み込むため) = 11クエリーが実行されます。
N + 1クエリー問題の解決
Active Recordは、開発者に予め全ての関連付けを指定してもらうことで、それを読み込むようにします。
これはModel.find
呼び出しのincludes
メソッドの指定によって可能になります。
includes
を使用することで、Active Recordは指定された関連付けの全てを、
可能な限り少ないクエリーで読み込みます。
上記のコードのClient.limit(10)
を、Eager loadingを使用して書き換えてみましょう。
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
上記のコードであれば、以前の11のクエリーに対し、2つのクエリーしか実行しません。
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
13.1 複数の関連付けによるEager Loading
Active Recordは、includes
メソッドを使用した単一のModel.find
の呼び出しで、
配列・ハッシュ、または配列/ハッシュの入れ子のハッシュによる複数の関連付けのEager loadingを行ってくれます。
13.1.1 複数の関連付けの配列
Post.includes(:category, :comments)
これは全ての投稿(post)と、各投稿に関連付けられたカテゴリー(category)とコメント(comment)を読み込みます。
13.1.2 関連付けハッシュの入れ子
Category.includes(posts: [{comments: :guest}, :tags]).find(1)
これはidが1であるカテゴリーと、Eager Loadingで全ての関連付けられた投稿(post)、 投稿に関連付けられたタグ(tag)とコメント(comment)、そして各コメントのゲストを見つけ出します。
13.2 Eager Loadingで関連付けられた上での条件指定
Active RecordはEager Loading上でjoins
のように条件を指定することが可能ですが、
joins
の代わりに使用する事をお勧めします。(翻訳に自信なし)
これを行う必要がある場合は、通常通りwhere
を使用することが可能です。
Post.includes(:comments).where("comments.visible" => true)
これがLEFT OUTER JOIN
を含むクエリーを生成するのに対し、
joins
メソッドは代わりにINNER JOIN
を使用した1つのものを生成します。
SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1)
もし、where
条件が無ければ、通常の2つのクエリーのセットを生成します。
このincludes
のクエリーのケースであれば、投稿(post)へのコメントが何も無くても、
全ての投稿(post)情報を読み込みます。
joins
(INNER JOIN)を使用すると、連結条件は一致しなければならず、
一致しない場合はレコードは返されません。
14. スコープ
スコープ(scope
)は、共通で使用する(よく使われる)クエリーを指定することで、
関連付けられたオブジェクトまたはモデル上でメソッド呼び出しとして参照する事が出来るようにしてくれます。
where
、joins
、includes
のような各メソッドを予め定義しておく事を可能にしてくれます。
全てのscope
メソッドは、更にメソッド(他のscopeのような)が呼び出せるように、
ActiveRecord::Relation
オブジェクトを返してくれます。
シンプルなスコープを定義するには、クラス内にscope
メソッドを使用し、
このスコープが呼び出された際に実行したいクエリーを渡します。
class Post < ActiveRecord::Base
scope :published, -> { where(published: true) }
end
これはクラスメソッド定義と全く同義で、 どちらを使うかは個人の好みになります。
class Post < ActiveRecord::Base
def self.published
where(published: true)
end
end
スコープはまた、スコープの内部でもチェーン可能です。
class Post < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
このpublished
スコープをクラス、
Post.published # => [published posts]
または関連付けされたPostオブジェクトのいずれかから呼び出すことが出来ます。
category = Category.first
category.posts.published # => [published posts belonging to this category]
14.1 引数について
スコープに引数を渡すことも出来ます。
class Post < ActiveRecord::Base
scope :created_before, ->(time) { where("created_at < ?", time) }
end
これは、下記のようにして呼び出すことが出来ます。
Post.created_before(Time.zone.now)
ただし、これは単にクラスメソッドによって提供される機能の複製になります。
class Post < ActiveRecord::Base
def self.created_before(time)
where("created_at < ?", time)
end
end
スコープが引数を受け取る場合には、クラスメソッドを使用するほうが好ましいです。 これらのメソッドは、関連付けオブジェクト上からでもアクセスすることが出来ます。
category.posts.created_before(time)
14.2 スコープのマージ
where
と同様、スコープはAND
条件を使用してマージされます。
class User < ActiveRecord::Base
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.active.inactive
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
スコープとwhere
条件を織り交ぜて、
最終的なSQLが全ての条件をAND
で繋げるようにすることが出来ます。
User.active.where(state: 'finished')
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
もし、最後のWHERE
句だけが必要な場合は、Relation#merge
を使用することが出来ます。
User.active.merge(User.inactive)
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
default_scope
は、スコープとwhere
条件で上書きされることに注意してください。
class User < ActiveRecord::Base
default_scope { where state: 'pending' }
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.all
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
User.active
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
User.where(state: 'inactive')
# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
上記の通り、default_scope
はscope
とwhere
の両方で上書きされます。
14.3 デフォルトスコープの適用
もし、モデルを通して全てのクエリーにスコープが適用されるようにしたいのであれば、
モデル内でdefault_scope
メソッドを使用することで、それを行うことが可能です。
class Client < ActiveRecord::Base
default_scope { where("removed_at IS NULL") }
end
このモデルでクエリーが実行されると、SQLクエリーは下記のようになります。
SELECT * FROM clients WHERE removed_at IS NULL
もし、デフォルトスコープ(default_scope)でより複雑な事をする必要がある場合は、 代わりにクラスメソッドとして定義することが出来ます。
class Client < ActiveRecord::Base
def self.default_scope
# ActiveRecord::Relationを返す必要があります。
end
end
14.4 全てのスコープを削除
もし、何らかの理由でスコープの適用を削除したい場合には、unscoped
メソッドを使用することが出来ます。
これは、モデル内にdefault_scope
が指定されていて、
特定のクエリーに対してそれを適用したくない場合に便利です。
Client.unscoped.all
このメソッドは全てのスコープを削除し、テーブル上で通常のクエリーを実行します。
unscoped
とscope
のチェーンは動作しないことに注意してください。
こういったケースでは、ブロック形式のunscoped
の使用をお勧めします。
Client.unscoped {
Client.created_before(Time.zone.now)
}
15. 動的ファインダー
動的なfindはRails4.0で非推奨になり、Rails4.1で削除される予定なので注意してください。 代わりにActive Recordのレコードを使用することが推奨されています。 非推奨になったGemは、 https://github.com/rails/activerecord-deprecated_findersにあります。
テーブル内で定義した各フィールド(属性とも言う)に対し、Active Recordはfind系のメソッドを提供します。
例えばもし、Clientモデルにfirst_name
と呼ばれるフィールドがある場合、
Active Recordからfind_by_first_name
というメソッドが提供されます。
また、Clientモデルにlocked
フィールドがあれば、find_by_locked
が提供されます。
感嘆符(!)をClient.find_by_name!("Ryan")
のようにして動的findの最後に付けることで、
もし該当するレコードが1件も無ければ、
ActiveRecord::RecordNotFound
エラーを発生させることが出来るようになります。
もし、name
とlocked
の両方でfindを使用したい場合は、
その2つのフィールドの間に単に"and
"を入れるだけで、それらがチェーンされます。
例えば、Client.find_by_first_name_and_locked("Ryan", true)
となります。
16. findまたはbuildによる新しいオブジェクト
レコードを探し、もし存在しなければレコードを新規作成したいといったケースがよくあります。
これは、find_or_create_by
とfind_or_create_by!
メソッドを使うことで行うことが出来ます。
16.1 find_or_create_by
find_or_create_by
メソッドは、その属性を持つレコードが存在するか否かを調べます。
もし存在しなければ、create
が呼び出されます。下記の例を見てみましょう。
'Andy'という名前の顧客(client)を見つけたいと仮定すると、
その顧客が存在しなかった場合にその名前の顧客がcreate
されます。
Client.find_or_create_by(first_name: 'Andy')
# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
このメソッドによって、次のようなSQLが生成されます。
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
find_or_create_by
は、既に存在するレコードか新しいレコードのどちらかを返します。
このケースでは、Andyという名前の顧客が存在しなかったため、新しいレコードが作成されて返されました。
新しいレコードは、バリデーション(validation)をパスしたか否かに依って、
データベースに保存されていない(create
のように)かもしれません。
新しいレコードを作成する際に'locked'属性はtrueに設定したいが、 このクエリーにはそれを含めたくないと仮定します。 そのため、"Andy"という名前の顧客を見つけ、 存在しなければ"Andy"という名前のロック(locked)されていない顧客を作成します。
これを行うための2つの方法が用意されています。
1つ目は、create_with
の使用です。
Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
2つ目は、ブロックを使用する方法です。
Client.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
ブロックは顧客が作成される際にのみ、実行されます。 2回目のこのコードの実行では、ブロックは無視されます。
16.2 find_or_create_by!
find_or_create_by!
を使用して、新しいレコードが無効(invalid)だった場合に、例外を発生させることも可能です。
検証(Validation)についてはこのガイドでは説明しませんが、ひとまず下記のコードをClientモデルに追加してみましょう。
validates :orders_count, presence: true
もし、新しい顧客(Client)をorders_count
を渡さずに作成しようとすると、
そのレコードは無効(invalid)となり、例外が発生します。
Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
16.3 find_or_initialize_by
find_or_initialize_by
メソッドは、find_or_create_by
のように動作しますが、
create
の代わりにnew
を呼び出します。
これは、メモリー内に新しいモデルのインスタンスを作成しますが、データベースには保存しない事を意味します。
find_or_create_by
と同じく、ここでは'Nick'という名前を指定します。
nick = Client.find_or_initialize_by(first_name: 'Nick')
# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
nick.new_record?
# => true
オブジェクトはこの時点ではデータベース内に保存しないので、SQLは下記のように生成されます。
SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
これをデータベースに保存したい場合は、save
を呼び出します。
nick.save
# => true
17. SQL文によるfind
もし直接SQLを書いてテーブルからレコードを見つけたい場合は、find_by_sql
を使用することが出来ます。
find_by_sql
メソッドは、クエリーが返すレコードが単一のものであっても、オブジェクトの配列を返します。
例えば、このようにしてクエリーを実行することが出来ます。
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc")
find_by_sql
は、データベースへの任意のクエリーを実行して、インスタンスオブジェクトを取得するシンプルな方法を提供します。
17.1 select_all
find_by_sql
は、connection#select_all
と近い関係にあります。
select_all
はデータベースからfind_by_sql
任意のSQLを使用してオブジェクトを取得しますが、
それらはインスタンス化を行いません。
代わりに、各レコードを指し示すハッシュの配列を取得します。
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
17.2 pluck
pluck
は、モデルのテーブルから1つまたは複数のカラムを参照するのに使用されます。
これはカラム名のリストを引数として受け取り、データ型に沿ったカラム固有の値の配列を返します。
Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
pluck
は、下記のようなコードにも置き換える事が出来ます。
Client.select(:id).map { |c| c.id }
# or
Client.select(:id).map(&:id)
# or
Client.select(:id, :name).map { |c| [c.id, c.name] }
Client.pluck(:id)
# or
Client.pluck(:id, :name)
select
と異なり、pluck
はActiveRecordオブジェクトにコンストラクタすること無く、
直接データベースの結果をRubyの配列に変換します。
これは、規模の大きいまたは回数の多いクエリーのパフォーマンスが向上することに繋がります。
ただし、モデルメソッドの上書きでは利用出来ません。
例えば、
class Client < ActiveRecord::Base
def name
"I am #{super}"
end
end
Client.select(:name).map &:name
# => ["I am David", "I am Jeremy", "I am Jose"]
Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]
更にselect
や、その他のRelation
スコープと異なり、
pluck
は即座にクエリーを実行するため、それ以降スコープをチェーンすることが出来ませんが、
予めコンストラクトしたスコープと動作させる事は可能です。
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
Client.limit(1).pluck(:name)
# => ["David"]
17.3 ids
ids
は、テーブルの主キーを使用して、関連付けのために全てのIDを取り出すのに使用することが出来ます。
Person.ids
# SELECT id FROM people
#主キーが"id"では無い場合
class Person < ActiveRecord::Base
self.primary_key = "person_id"
end
Person.ids
# SELECT person_id FROM people
18. オブジェクトの存在
もし、オブジェクト(レコード)が単に存在するか否かを確認したいのであれば、exists?
メソッドでそれを行うことが出来ます。
このメソッドは、findとして同じクエリーをデータベースに対して行いますが、
オブジェクトまたはオブジェクトのコレクションを返す代わりに、trueかfalseのいずれかを返します。
Client.exists?(1)
また、exists?
メソッドは複数のidを受け取りますが、
いずれかのレコードが存在すればtrueを返します。
Client.exists?(1,2,3)
# or
Client.exists?([1,2,3])
モデルまたはRelation上では、引数無しでもexists?
を使用することが可能です。
Client.where(first_name: 'Ryan').exists?
上記はfirst_name
が'Ryan'の顧客が少なくとも1人存在すれば、
trueを返し、そうでなければfalseを返します。
Client.exists?
上記は、Clientテーブルが空であればfalseを返し、そうでなければtrueを返します。
any?
とmany?
を使用して、モデルまたはRelation上で存在の確認をすることも出来ます。
# モデルを通して
Post.any?
Post.many?
# 名付けされたスコープを通して
Post.recent.any?
Post.recent.many?
# Relationを通して
Post.where(published: true).any?
Post.where(published: true).many?
# 関連付けを通して
Post.first.categories.any?
Post.first.categories.many?
19. 計算
このセクションでは、最初にcount
を例としてオプションの説明をしますが、
以降のメソッド全てにおいて、ここで説明するオプションが適用されます。
全ての計算メソッドはモデル上で直接動作します。
Client.count
# SELECT count(*) AS count_all FROM clients
または、Relation上で、
Client.where(first_name: 'Ryan').count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
Relation上で、複雑な計算を実行するために様々なfind系メソッドを使用することも可能です。
Client.includes("orders").where(first_name: 'Ryan', orders: {status: 'received'}).count
これは、下記のように実行されます。
SELECT count(DISTINCT clients.id) AS count_all FROM clients
LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
(clients.first_name = 'Ryan' AND orders.status = 'received')
19.1 Count
もし、モデルのテーブル内にいくつのレコードがあるのか確認したい場合は、Client.count
を呼び出すことで、
その件数を取得することが可能です。
もし、更に条件を絞り込んで、データベース内の年齢(age)が入力されている全ての顧客を見つけたい場合は、
Client.count(:age).
を使用することが出来ます。
オプションについては、このセクションの最初のオプションの説明を参照してください。
19.2 Average
テーブルに関連付けられたクラスからaverage
メソッドを呼び出す事で、テーブル内のある数の平均値を確認することが出来ます。
このメソッドは、下記のようにして呼び出します。
Client.average("orders_count")
これは、フィールドの平均を表す数値(3.14159265のような浮動小数点数の可能性があります)を返します。
オプションについては、このセクションの最初のオプションの説明を参照してください。
19.3 Minimum
テーブルに関連付けられたクラスからminimum
メソッドを呼び出す事で、テーブル内のある数の最小値を確認することが出来ます。
このメソッドは、下記のようにして呼び出します。
Client.minimum("age")
オプションについては、このセクションの最初のオプションの説明を参照してください。
19.4 Maximum
テーブルに関連付けられたクラスからmaximum
メソッドを呼び出す事で、テーブル内のある数の最大値を確認することが出来ます。
このメソッドは、下記のようにして呼び出します。
Client.maximum("age")
オプションについては、このセクションの最初のオプションの説明を参照してください。
19.5 Sum
テーブルに関連付けられたクラスからsum
メソッドを呼び出す事で、テーブル内の全てのレコードの合計値を確認することが出来ます。
このメソッドは、下記のようにして呼び出します。
Client.sum("orders_count")
オプションについては、このセクションの最初のオプションの説明を参照してください。
20. EXPLAINの実行
Relationにより、クエリー上でEXPLAIN
を実行させる事が可能です。
例えば、MySQLであれば、
User.where(id: 1).joins(:posts).explain
次のように実行されます。
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
2 rows in set (0.00 sec)
Active Recordは、データベースシェルをエミュレートした内容が、そのまま出力されるように実行します。 そのため、同じクエリーであってもPostgreSQLのアダプターが使用されていれば、 代わりに次のように出力されます。
EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
Join Filter: (posts.user_id = users.id)
-> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
Index Cond: (id = 1)
-> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
Filter: (posts.user_id = 1)
(6 rows)
Eager Loadingは裏で複数のクエリーをトリガする可能性があり、
クエリーによっては事前にその結果が必要になる事があります。
そのためexplain
は実際にそのクエリーを実行して、
クエリープランの問い合わせを行います。
例えば、MySQLであれば
User.where(id: 1).includes(:posts).explain
下記のように出力されます。
EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1)
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
20.1 EXPLAINについての解説
EXPLAIN
の出力の解説については、このガイドの範疇を超えているため、
下記のリンク先を参照してください。
- SQLite3: EXPLAIN QUERY PLAN
- MySQL: EXPLAIN Output Format
- PostgreSQL: Using EXPLAIN
© 2010 - 2017 STUDIO KINGDOM