Active Record クエリインターフェイス

このガイドでは、Active Recordを使用してデータベースからデータを取り出すためのさまざまな方法について解説します。

このガイドの内容:

目次

  1. データベースからオブジェクトを取り出す
  2. 条件
  3. 並び順
  4. 特定のフィールドだけを取り出す
  5. LimitとOffset
  6. グループ
  7. Having
  8. 条件を上書きする
  9. Nullリレーション
  10. 読み取り専用オブジェクト
  11. レコードを更新できないようロックする
  12. テーブルを結合する
  13. 関連付けを一括読み込みする
  14. スコープ
  15. 動的検索
  16. Enums
  17. メソッドチェインを理解する
  18. 新しいオブジェクトを検索またはビルドする
  19. SQLで検索する
  20. オブジェクトの存在チェック
  21. 計算
  22. EXPLAINを実行する

生のSQLを使用してデータベースのレコードを検索することに慣れきった人がRailsに出会うと、Railsでは同じ操作をずっと洗練された方法で実現できることに気付くでしょう。Active Recordを使用することで、SQLを直に実行する必要はほぼなくなります。

本ガイドのコード例では、基本的に以下のモデルを使用します。

特に記さない限り、モデル中のidは主キーを表します。

class Client < ApplicationRecord
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end
class Address < ApplicationRecord
  belongs_to :client
end
class Order < ApplicationRecord
  belongs_to :client, counter_cache: true
end
class Role < ApplicationRecord
  has_and_belongs_to_many :clients
end

Active Recordは、ユーザーに代わってデータベースにクエリを発行します。発行されるクエリは多くのデータベースシステム (MySQL、MariaDB、PostgreSQL、SQLiteなど) と互換性があります。Active Recordを使用していれば、利用しているデータベースシステムの種類にかかわらず、同じ表記を使用できます。

1 データベースからオブジェクトを取り出す

Active Recordでは、データベースからオブジェクトを取り出すための検索メソッドを多数用意しています。これらの検索メソッドを使用することで、生のSQLを書くことなく、データベースへの特定のクエリを実行するための引数を渡すことができます。

以下のメソッドが用意されています。

  • find
  • create_with
  • distinct
  • eager_load
  • extending
  • from
  • group
  • having
  • includes
  • joins
  • left_outer_joins
  • limit
  • lock
  • none
  • offset
  • order
  • preload
  • readonly
  • references
  • reorder
  • reverse_order
  • select
  • where

検索メソッドはwheregroupといったコレクションを返したり、ActiveRecord::Relationのインスタンスを返します。また、findfirstなどの1つのエンティティを検索するメソッドの場合、そのモデルのインスタンスを返します。

Model.find(options)という操作を要約すると以下のようになります。

  • 与えられたオプションを同等のSQLクエリに変換します。
  • SQLクエリを発行し、該当する結果をデータベースから取り出します。
  • 得られた結果を行ごとに同等のRubyオブジェクトとしてインスタンス化します。
  • 指定されていれば、after_findを実行し、続いてafter_initializeコールバックを実行します。

1.1 単一のオブジェクトを取り出す

Active Recordには、単一のオブジェクトを取り出すためのさまざま方法が用意されています。

1.1.1 find

findメソッドを使用すると、与えられたどのオプションにもマッチする 主キー に対応するオブジェクトを取り出すことができます。以下に例を示します。

# 主キー(id)が10のクライアントを検索
clients = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">

これと同等のSQLは以下のようになります。

SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1

findメソッドでマッチするレコードが見つからない場合、ActiveRecord::RecordNotFound例外が発生します。

このメソッドを使用して、複数のオブジェクトへのクエリを作成することもできます。これを行うには、findメソッドの呼び出し時に主キーの配列を渡します。これにより、与えられた 主キー にマッチするレコードをすべて含む配列が返されます。以下に例を示します。

# 主キー(id)が1と10のクライアントを検索
clients = Client.find([1, 10]) # 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))

findメソッドで与えられた主キーの中に、どのレコードにもマッチしない主キーが1つでもあると、ActiveRecord::RecordNotFound例外が発生します。

1.1.2 take

takeメソッドはレコードを1つ取り出します。どのレコードが取り出されるかは指定されません。以下に例を示します。

client = Client.take
# => #<Client id: 1, first_name: "Lifo">

これと同等のSQLは以下のようになります。

SELECT * FROM clients LIMIT 1

Model.takeは、モデルにレコードが1つもない場合にnilを返します。このとき例外は発生しません。

takeメソッドで返すレコードの最大数を数値の引数で指定することもできます。例:

clients = Client.take(2)
# => [
#   #<Client id: 1, first_name: "Lifo">,
#   #<Client id: 220, first_name: "Sara">
# ]

これと同等のSQLは以下のようになります。

SELECT * FROM clients LIMIT 2

take!メソッドの動作は、takeメソッドとまったく同じです。ただし、take!メソッドでは、マッチするレコードが見つからない場合にActiveRecord::RecordNotFound例外が発生する点だけが異なります。

このメソッドで取り出されるレコードは、使用するデータベースエンジンによっても異なることがあります。

1.1.3 first

firstメソッドは、デフォルトでは主キー順の最初のレコードを取り出します。以下に例を示します。

clients = Client.first
# => #<Client id: 1, first_name: "Lifo">

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1

firstメソッドは、モデルにレコードが1つもない場合にnilを返します。このとき例外は発生しません。

デフォルトスコープが順序に関するメソッドを含んでいる場合、firstメソッドはその順序に従って最初のレコードを返します。

firstメソッドで返すレコードの最大数を数値の引数で指定することもできます。例:

clients = Client.first(3)
# => [
#   #<Client id: 1, first_name: "Lifo">,
#   #<Client id: 2, first_name: "Fifo">,
#   #<Client id: 3, first_name: "Filo">
# ]

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3

orderを使って順序を変更したコレクションの場合、firstメソッドはorderで指定された属性に従って最初のレコードを返します。

client = Client.order(:first_name).first
# => #<Client id: 2, first_name: "Fifo">

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1

first!メソッドの動作は、マッチするレコードが見つからない場合にActiveRecord::RecordNotFound例外が発生する点を除いて、firstメソッドとまったく同じです。

1.1.4 last

lastメソッドは、(デフォルトでは) 主キーの順序に従って最後のレコードを返します。 以下に例を示します。

client = Client.last
# => #<Client id: 221, first_name: "Russel">

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1

lastメソッドは、モデルにレコードが1つもない場合にnilを返します。このとき例外は発生しません。

デフォルトスコープが順序に関するメソッドを含んでいる場合、lastメソッドはその順序に従って最後のレコードを返します。

lastメソッドで返すレコードの最大数を数値の引数で指定することもできます。例:

clients = Client.last(3)
# => [
#   #<Client id: 219, first_name: "James">,
#   #<Client id: 220, first_name: "Sara">,
#   #<Client id: 221, first_name: "Russel">
# ]

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3

orderを使って順序を変更したコレクションの場合、lastメソッドはorderで指定された属性に従って最後のレコードを返します。

client = Client.order(:first_name).last
# => #<Client id: 220, first_name: "Sara">

これと同等のSQLは以下のようになります。

SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1

last!メソッドの動作は、マッチするレコードが見つからない場合にActiveRecord::RecordNotFound例外が発生する点を除いて、lastメソッドとまったく同じです。

1.1.5 find_by

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

これと同等のSQLは以下のようになります。

SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1

find_by!メソッドの動作は、マッチするレコードが見つからない場合にActiveRecord::RecordNotFound例外が発生する点を除いて、find_byメソッドとまったく同じです。以下に例を示します。

Client.find_by! first_name: 'does not exist'
# => ActiveRecord::RecordNotFound

上の文は以下のように書くこともできます。

Client.where(first_name: 'does not exist').take!

1.2 複数のオブジェクトをバッチで取り出す

多数のレコードに対して反復処理を行いたいことがあります。たとえば、多くのユーザーにニュースレターを送信したい、データをエクスポートしたいなどです。

このような処理をそのまま実装すると以下のようになるでしょう。

# このコードはテーブルが大きい場合、メモリを大量に消費する可能性あり
User.all.each do |user|
  NewsMailer.weekly(user).deliver_now
end

しかし上のような処理は、テーブルのサイズが大きくなるにつれて非現実的になります。User.all.eachは、Active Recordに対して テーブル全体 を一度に取り出し、しかも1行ごとにオブジェクトを生成し、その巨大なモデルオブジェクトの配列をメモリに配置するからです。もし莫大な数のレコードに対してこのようなコードをまともに実行すると、コレクション全体のサイズがメモリ容量を上回ってしまうことでしょう。

Railsでは、メモリを圧迫しないサイズにバッチを分割して処理するための方法を2とおり提供しています。1つ目はfind_eachメソッドを使用する方法です。これは、レコードのバッチを1つ取り出し、次に レコードを1つのモデルとして個別にブロックにyieldします。2つ目の方法はfind_in_batchesメソッドを使用する方法です。レコードのバッチを1つ取り出し、次に バッチ全体 をモデルの配列としてブロックにyieldします。

find_eachメソッドとfind_in_batchesメソッドは、一度にメモリに読み込めないような大量のレコードに対するバッチ処理のためのものです。数千のレコードに対して単にループ処理を行なうのであれば通常の検索メソッドで十分です。

1.2.1 find_each

find_eachメソッドは、複数のレコードを一括で取り出し、続いて レコードを1つのブロックにyieldします。以下の例では、find_eachでバッチから1000件のレコードを一括で取り出し、各レコードをブロックにyieldします。

User.find_each do |user|
  NewsMailer.weekly(user).deliver_now
end

この処理は、必要に応じてさらにレコードのまとまりをフェッチし、すべてのレコードが処理されるまで繰り返されます。

find_eachメソッドは上述のようにモデルのクラスに対して機能します。対象がリレーションの場合も同様です。

User.where(weekly_subscriber: true).find_each do |user|
  NewsMailer.weekly(user).deliver_now
end

ただしこれは順序指定がない場合に限ります。find_eachメソッドでイテレートするには内部で順序を強制する必要があるためです。

レシーバー側に順序制約がある場合、config.active_record.error_on_ignored_orderフラグの状態によって振る舞いが変わります。たとえばtrueの場合はArgumentErrorが発生し、falseの場合は順序が無視され警告が発生します。デフォルトはfalseです。このフラグを上書きしたい場合は:error_on_ignoreオプション(後述)を使います。

1.2.1.1 find_eachのオプション

:batch_size

:batch_sizeオプションは、(ブロックに個別に渡される前に) 1回のバッチで取り出すレコード数を指定します。たとえば、1回に5000件ずつ処理したい場合は以下のように指定します。

User.find_each(batch_size: 5000) do |user|
  NewsMailer.weekly(user).deliver_now
end

:start

デフォルトでは、レコードは主キーの昇順に取り出されます。主キーは整数でなければなりません。並び順冒頭のIDが不要な場合、:startオプションを使用してシーケンスの開始IDを指定します。これは、たとえば中断したバッチ処理を再開する場合などに便利です (最後に実行された処理のIDがチェックポイントとして保存済みであることが前提です)。

たとえば主キーが2000番以降のユーザーに対してニュースレターを配信する場合は、以下のようになります。

User.find_each(start: 2000) do |user|
  NewsMailer.weekly(user).deliver_now
end

:finish

:startオプションと同様に、シーケンスの末尾のIDを指定したい場合は、:finishオプションで末尾のIDを設定できます。 :start:finishでレコードのサブセットを指定し、その中でバッチプロセスを走らせたい時に便利です。

たとえば主キーが2000番〜10000番のユーザーに対してニュースレターを配信したい場合は、以下のようになります。

User.find_each(start: 2000, finish: 10000) do |user|
  NewsMailer.weekly(user).deliver_now
end

他にも、同じ処理キューを複数の作業者で手分けする場合が考えられます。たとえば各ワーカーに10000レコードずつ処理して欲しい場合も、:start:finishオプションにそれぞれ適切な値を設定することで実現できます。

:error_on_ignore

リレーション内に順序制約があれば例外を発生させたい場合は、このオプションを使ってアプリケーションの設定を上書きしてください。

1.2.2 find_in_batches

find_in_batchesメソッドは、レコードをバッチで取り出すという点でfind_eachと似ています。違うのは、find_in_batchesバッチ を個別にではなくモデルの配列としてブロックにyieldするという点です。以下の例では、与えられたブロックに対して一度に最大1000までの納品書 (invoice) の配列をyieldしています。最後のブロックには残りの納品書が含まれます。

# 1回あたりadd_invoicesに納品書1000通の配列を渡す
Invoice.find_in_batches do |invoices|
  export.add_invoices(invoices)
end

find_in_batchesメソッドは上述のようにモデルのクラスに対して機能します。対象がリレーションの場合も同様です。

Invoice.pending.find_in_batches do |invoices|
  pending_invoices_export.add_invoices(invoices)
end

ただしこれは順序指定がない場合に限ります。find_in_batchesメソッドでイテレートするには内部で順序を強制する必要があるためです。

1.2.2.1 find_in_batchesのオプション

find_in_batchesメソッドでは、find_eachメソッドと同様のオプションを使えます。

2 条件

whereメソッドは、返されるレコードを制限するための条件を指定します。SQL文で言うWHEREの部分に相当します。条件は、文字列、配列、ハッシュのいずれかの方法で与えることができます。

2.1 文字列だけで表された条件

検索メソッドに条件を追加したい場合、たとえばClient.where("orders_count = '2'")のように条件を単純に指定することができます。この場合、orders_countフィールドの値が2であるすべてのクライアントが検索されます。

条件を文字列だけで構成すると、SQLインジェクションの脆弱性が発生する可能性があります。たとえば、Client.where("first_name LIKE '%#{params[:first_name]}%'")という書き方は危険です。次で説明するように、配列を使用するのが望ましい方法です。

2.2 配列で表された条件

条件で使用する数値が変動する可能性がある場合、引数をどのようにすればよいでしょうか。この場合は以下のようにします。

Client.where("orders_count = ?", params[:orders])

Active Recordは最初の引数を、文字列で表された条件として受け取ります。その後に続く引数は、文字列内にある疑問符 ? と置き換えられます。

複数の条件を指定したい場合は次のようにします。

Client.where("orders_count = ? AND locked = ?", params[:orders], false)

上の例では、1つ目の疑問符はparams[:orders]の値で置き換えられ、2つ目の疑問符はfalseをSQL形式に変換したもの (変換方法はアダプタによって異なる) で置き換えられます。

以下のようなコードの書き方を強く推奨します。

Client.where("orders_count = ?", params[:orders])

以下の書き方は危険であり、避ける必要があります。

Client.where("orders_count = #{params[:orders]}")

条件文字列の中に変数を直接置くと、その変数はデータベースに そのまま 渡されてしまいます。これは、悪意のある人物がエスケープされていない危険な変数を渡すことができるということです。このようなコードがあると、悪意のある人物がデータベースを意のままにすることができ、データベース全体が危険にさらされます。くれぐれも、条件文字列の中に引数を直接置くことはしないでください。

SQLインジェクションの詳細についてはRuby on Railsセキュリティガイドを参照してください。

2.2.1 プレースホルダを使用した条件

疑問符(?)をパラメータで置き換えるスタイルと同様、条件中でキー/値のハッシュを渡すことができます。ここで渡されたハッシュは、条件中の対応するキー/値の部分に置き換えられます。

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)

これは以下のようなSQLを生成します。

SELECT * FROM clients WHERE (clients.locked = 1)

フィールド名は文字列形式にすることもできます。

Client.where('locked' => true)

belongs_toリレーションシップの場合、Active Recordオブジェクトが値として使用されていれば、モデルを指定する時に関連付けキーを使用できます。この方法はポリモーフィックリレーションシップでも同様に使用できます。

Article.where(author: author)
Author.joins(:articles).where(articles: { author: author })
2.3.2 範囲条件
Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

上の例では、昨日作成されたすべてのクライアントを検索します。内部ではSQLのBETWEEN文が使用されます。

SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')

配列で表された条件では、さらに簡潔な文例をご紹介しています。

2.3.3 サブセット条件

SQLのIN式を使用してレコードを検索したい場合、条件ハッシュにそのための配列を1つ渡すことができます。

Client.where(orders_count: [1,3,5])

上のコードを実行すると、以下のようなSQLが生成されます。

SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))

2.4 NOT条件

SQLのNOTクエリは、where.notで表せます。

Client.where.not(locked: true)

言い換えれば、このクエリはwhereに引数を付けずに呼び出し、直後にwhere条件にnotを渡してチェインすることで生成されています。これは以下のようなSQLを出力します。

SELECT * FROM clients WHERE (clients.locked != 1)

2.5 OR条件

2つのリレーションをまたいでOR条件を使いたい場合は、1つ目のリレーションでorメソッドを呼び出し、そのメソッドの引数に2つ目のリレーションを渡すことで実現できます。

Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))
SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))

3 並び順

データベースから取り出すレコードを特定の順序で並べ替えたい場合、orderを使用できます。

たとえば、ひとかたまりのレコードを取り出し、それをテーブル内のcreated_atの昇順で並べたい場合には以下のようにします。

Client.order(:created_at)
# または
Client.order("created_at")

ASC(昇順)やDESC(降順)を指定することもできます。

Client.order(created_at: :desc)
# または
Client.order(created_at: :asc)
# または
Client.order("created_at DESC")
# または
Client.order("created_at ASC")

複数のフィールドを指定して並べることもできます。

Client.order(orders_count: :asc, created_at: :desc)
# または
Client.order(:orders_count, created_at: :desc)
# または
Client.order("orders_count ASC, created_at DESC")
# または
Client.order("orders_count ASC", "created_at DESC")

orderメソッドを複数回呼び出すと、続く並び順は最初の並び順に追加されていきます。

Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC

MySQL 5.7.5以上のバージョンで selectpluckidsメソッドを使ってフィールドを選択し、かつ、選択しているリストにorder句を使ったフィールドが含まれていないと、orderメソッドでActiveRecord::StatementInvalid例外が発生します。結果から特定のフィールドだけを取り出す方法については、次のセクションを参照してください。

4 特定のフィールドだけを取り出す

デフォルトでは、Model.findを実行すると、結果セットからすべてのフィールドが選択されます。内部的にはSQLのselect *が実行されています。

結果セットから特定のフィールドだけを取り出したい場合、selectメソッドを使用できます。

たとえば、viewable_byカラムとlockedカラムだけを取り出したい場合は以下のようにします。

Client.select("viewable_by, locked")

上で実際に使用されるSQL文は以下のようになります。

SELECT viewable_by, locked FROM clients

selectを使用すると、選択したフィールドだけを使用してモデルオブジェクトが初期化されるため、注意してください。モデルオブジェクトの初期化時に指定しなかったフィールドにアクセスしようとすると、以下のメッセージが表示されます。

ActiveModel::MissingAttributeError: missing attribute: <属性名>

<属性名>は、アクセスしようとした属性です。idメソッドは、このActiveRecord::MissingAttributeErrorを発生しません。このため、関連付けを扱う場合には注意してください。関連付けが正常に動作するにはidメソッドが必要だからです。

特定のフィールドについて、重複のない一意の値を1レコードだけ取り出したい場合、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を適用したい場合、リレーションでlimitメソッドとoffsetメソッドを使用することでLIMITを指定できます。

limitメソッドは、取り出すレコード数の上限を指定します。offsetは、レコードを返す前にスキップするレコード数を指定します。例:

Client.limit(5)

上を実行するとクライアントが最大で5つ返されます。オフセットは指定されていないので、最初の5つがテーブルから取り出されます。この時実行されるSQLは以下のような感じになります。

SELECT * FROM clients LIMIT 5

offsetを追加すると以下のようになります。

Client.limit(5).offset(30)

上のコードは、最初の30クライアントをスキップして31人目から最大5人のクライアントを返します。このときのSQLは以下のようになります。

SELECT * FROM clients LIMIT 5 OFFSET 30

6 グループ

検索メソッドで実行されるSQLにGROUP BY句を追加したい場合は、groupメソッドを検索メソッドに追加できます。

たとえば、注文 (order) の作成日のコレクションを検索したい場合は、以下のようにします。

Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")

上のコードは、データベースで注文のある日付ごとにOrderオブジェクトを1つ作成します。

上で実行されるSQLは以下のようなものになります。

SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)

6.1 グループ化された項目の合計

グループ化した項目の合計をひとつのクエリで得るには、groupの次にcountを呼び出します。

Order.group(:status).count
# => { 'awaiting_approval' => 7, 'paid' => 12 }

上で実行されるSQLは以下のようなものになります。

SELECT COUNT (*) AS count_all, status AS status
FROM "orders"
GROUP BY status

7 Having

SQLでは、GROUP BYフィールドで条件を指定する場合にHAVING句を使用します。検索メソッドで:havingメソッドを使用すると、Model.findで生成される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

これは各orderオブジェクトの注文日と合計金額を返します。具体的には、priceが$100を超えているものが、dateごとにまとめられて返されます。

8 条件を上書きする

8.1 unscope

unscopeを使用して特定の条件を取り除くことができます。以下に例を示します。

Article.where('id > 10').limit(20).order('id asc').unscope(:order)

上で実行されるSQLは以下のようなものになります。

SELECT * FROM articles WHERE id > 10 LIMIT 20

# `unscope`する前のオリジナルのクエリ
SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20

特定のwhere句でunscopeを指定することもできます。以下に例を示します。

Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0

unscopeをリレーションに適用すると、それにマージされるすべてのリレーションにも影響します。

Article.order('id asc').merge(Article.unscope(:order))
# SELECT "articles".* FROM "articles"

8.2 only

onlyメソッドを使用すると、条件を上書きできます。以下に例を示します。

Article.where('id > 10').limit(20).order('id desc').only(:order, :where)

上で実行されるSQLは以下のようなものになります。

SELECT * FROM articles WHERE id > 10 ORDER BY id DESC

# `only`を使用する前のオリジナルのクエリ
SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20

8.3 reorder

reorderメソッドは、デフォルトのスコープの並び順を上書きします。以下に例を示します。

class Article < ApplicationRecord
  has_many :comments, -> { order('posted_at DESC') }
end

Article.find(10).comments.reorder('name')

上で実行されるSQLは以下のようなものになります。

SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY name

reorder句が使われていない場合、実行されるSQLは以下のようになります。

SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC

8.4 reverse_order

reverse_orderメソッドは、並び順が指定されている場合に並び順を逆にします。

Client.where("orders_count > 10").order(:name).reverse_order

上で実行されるSQLは以下のようなものになります。

SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC

SQLクエリで並び順を指定する句がない場合にreverse_orderを実行すると、主キーの逆順になります。

Client.where("orders_count > 10").reverse_order

上で実行されるSQLは以下のようなものになります。

SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC

このメソッドは引数を取りません

8.5 rewhere

rewhereメソッドは、既存のwhere条件を上書きします。以下に例を示します。

Article.where(trashed: true).rewhere(trashed: false)

上で実行されるSQLは以下のようなものになります。

SELECT * FROM articles WHERE `trashed` = 0

rewhereの代わりにwhereを2回使用すると、結果が異なります。

Article.where(trashed: true).where(trashed: false)

上で実行されるSQLは以下のようなものになります。

SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0

9 Nullリレーション

noneメソッドは、チェイン (chain) 可能なリレーションを返します (レコードは返しません)。このメソッドから返されたリレーションにどのような条件をチェインさせても、常に空のリレーションが生成されます。これは、メソッドまたはスコープへのチェイン可能な応答が必要で、しかも結果を一切返したくない場合に便利です。

Article.none # 空のリレーションを返し、クエリを生成しない。
# visible_articles メソッドはリレーションを返すことが期待されている
@articles = current_user.visible_articles.where(name: params[:name])

def visible_articles
  case role
  when 'Country Manager'
    Article.where(country: country)
  when 'Reviewer'
    Article.published
  when 'Bad User'
    Article.none # => []またはnilを返すと、このコード例では呼び出し元のコードを壊してしまう
  end
end

10 読み取り専用オブジェクト

Active Recordには、返されたどのオブジェクトに対しても変更を明示的に禁止するreadonlyメソッドがあります。読み取り専用を指定されたオブジェクトに対する変更の試みはすべて失敗し、ActiveRecord::ReadOnlyRecord例外が発生します。

client = Client.readonly.first
client.visits += 1
client.save

上のコードでは clientに対して明示的にreadonlyが指定されているため、 visits の値を更新して client.saveを行なうとActiveRecord::ReadOnlyRecord例外が発生します。

11 レコードを更新できないようロックする

ロックは、データベースのレコードを更新する際の競合状態を避け、アトミックな (=中途半端な状態のない) 更新を行なうために有用です。

Active Recordには2とおりのロック機構があります。

  • 楽観的ロック (optimistic)
  • 悲観的ロック (pessimistic)

11.1 楽観的ロック (optimistic)

楽観的ロックでは、複数のユーザーが同じレコードを編集することを許し、データの衝突が最小限であることを仮定しています。この方法では、レコードがオープンされてから変更されたことがあるかどうかをチェックします。そのような変更が行われ、かつ更新が無視された場合、ActiveRecord::StaleObjectError例外が発生します。

楽観的ロックカラム

楽観的ロックを使用するには、テーブルにlock_versionという名前のinteger型カラムがある必要があります。Active Recordは、レコードが更新されるたびにlock_versionカラムの値を1ずつ増やします。更新リクエストが発生したときの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を発生

例外の発生後、この例外をレスキューすることで衝突を解決する必要があります。衝突の解決方法は、ロールバック、マージ、またはビジネスロジックに応じた解決方法のいずれかを使用してください。

ActiveRecord::Base.lock_optimistically = falseを設定するとこの動作をオフにできます。

ActiveRecord::Baseには、lock_versionカラム名を上書きするためのlocking_columnが用意されています。

class Client < ApplicationRecord
  self.locking_column = :lock_client_column
end

11.2 悲観的ロック (pessimistic)

悲観的ロックでは、データベースが提供するロック機構を使用します。リレーションの構築時にlockを使用すると、選択した行に対する排他的ロックを取得できます。lockを使用するリレーションは、デッドロック条件を回避するために通常トランザクションの内側にラップされます。

以下に例を示します。

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オプションの引数にします。

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は JOIN句のSQLを具体的に指定する2つの検索メソッドを提供しています。1つはjoins、もう1つはleft_outer_joinsです。joinsメソッドはINNER JOINやカスタムクエリに使われ、left_outer_joinsLEFT OUTER JOINを使ったクエリの生成に使われます。

12.1 joins

joinsメソッドには複数の使い方があります。

12.1.1 SQLフラグメント文字列を使用する

joinsメソッドの引数に生のSQLを指定することでJOIN句を指定できます。

Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")

これによって以下のSQLが生成されます。

SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
12.1.2 名前付き関連付けの配列/ハッシュを使用する

Active Recordでは、joinsメソッドを使用して関連付けでJOIN句を指定する際に、モデルで定義された関連付けの名前をショートカットとして使用できます (詳細はActive Recordの関連付けを参照)。

たとえば、以下のCategoryArticleCommentGuestTagモデルについて考えてみましょう。

class Category < ApplicationRecord
  has_many :articles
end

class Article < ApplicationRecord
  belongs_to :category
  has_many :comments
  has_many :tags
end

class Comment < ApplicationRecord
  belongs_to :article
  has_one :guest
end

class Guest < ApplicationRecord
  belongs_to :comment
end

class Tag < ApplicationRecord
  belongs_to :article
end

以下のすべてにおいて、INNER JOINを使用した結合クエリが期待どおりに生成されています。

12.1.2.1 単一関連付けを結合する
Category.joins(:articles)

上によって以下が生成されます。

SELECT categories.* FROM categories
  INNER JOIN articles ON articles.category_id = categories.id

上のSQLを日本語で書くと「記事 (article) のあるすべてのカテゴリーを含む、Categoryオブジェクトを1つ返す」となります。なお、同じカテゴリーに複数の記事がある場合、カテゴリーが重複します。重複のない一意のカテゴリーが必要な場合は、Category.joins(:articles).distinctを使用できます。

12.1.3 複数の関連付けを結合する
Article.joins(:category, :comments)

上によって以下が生成されます。

SELECT articles.* FROM articles
  INNER JOIN categories ON articles.category_id = categories.id
  INNER JOIN comments ON comments.article_id = articles.id

上のSQLを日本語で書くと、「カテゴリーが1つあり、かつコメントが1つ以上ある、すべての記事を返す」となります。こちらも、コメントが複数ある記事は複数回表示されます。

12.1.3.1 ネストした関連付けを結合する (単一レベル)
Article.joins(comments: :guest)

上によって以下が生成されます。

SELECT articles.* FROM articles
  INNER JOIN comments ON comments.article_id = articles.id
  INNER JOIN guests ON guests.comment_id = comments.id

上のSQLを日本語で書くと、「ゲストによるコメントが1つある記事をすべて返す」となります。

12.1.3.2 ネストした関連付けを結合する (複数レベル)
Category.joins(articles: [{ comments: :guest }, :tags])

上によって以下が生成されます。

SELECT categories.* FROM categories
  INNER JOIN articles ON articles.category_id = categories.id
  INNER JOIN comments ON comments.article_id = articles.id
  INNER JOIN guests ON guests.comment_id = comments.id
  INNER JOIN tags ON tags.article_id = articles.id

上のSQLを日本語で書くと「ゲストによってコメントされた記事 (articles) の中で、タグを含んでいるCategoryオブジェクトをすべて返す」となります。

12.1.4 結合されたテーブルで条件を指定する

標準の配列および文字列条件を使用して、結合テーブルに条件を指定することができます。ハッシュ条件の場合、結合テーブルで条件を指定する場合に特殊な構文を使用します。

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 })

このコードでは、昨日作成された注文 (order) を持つすべてのクライアントを検索します。ここでもSQLのBETWEEN式を使用しています。

12.2 left_outer_joins

関連レコードがあるかどうかにかかわらずレコードのセットを取得したい場合は、left_outer_joinsメソッドを使います。

Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')

上のコードは、以下のクエリを生成します。

SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id

上のSQLを日本語で書くと「著者 (authors) が記事 (posts) を持っているかどうかにかかわらず、すべての著者とその記事の数を返す」となります。

13 関連付けを一括読み込みする

一括読み込み (eager loading) とは、Model.findによって返されるオブジェクトに関連付けられたレコードを読み込むためのメカニズムであり、できるだけクエリの使用回数を減らすようにします。

N + 1クエリ問題

以下のコードについて考えてみましょう。クライアントを10人検索して郵便番号を表示します。

clients = Client.limit(10)

clients.each do |client|
  puts client.address.postcode
end

このコードは一見何の問題もないように見えます。しかし本当の問題は、実行されたクエリの回数が無駄に多いことなのです。上のコードでは、最初にクライアントを10人検索するのにクエリを1回発行し、次にそこから住所を取り出すのにクエリを10回発行しますので、合計で 11 回のクエリが発行されます。

N + 1クエリ問題を解決する

Active Recordは、読み込まれるすべての関連付けを事前に指定することができます。これは、Model.find呼び出しでincludesを指定することで実現できます。includesを指定すると、Active Recordは指定されたすべての関連付けが最小限のクエリ回数で読み込まれるようにしてくれます。

上の例で言うと、Client.limit(10)というコードを書き直して、住所が一括で読み込まれるようにします。

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 複数の関連付けを一括で読み込む

Active Recordは、1つのModel.find呼び出しで関連付けをいくつでも一括読み込みすることができます。これを行なうには、includesメソッドで配列、ハッシュ、または、配列やハッシュのネストしたハッシュを使用します。

13.1.1 複数の関連付けの配列
Article.includes(:category, :comments)

上のコードは、記事と、それに関連付けられたカテゴリやコメントをすべて読み込みます。

13.1.2 ネストした関連付けハッシュ
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)

上のコードは、id=1のカテゴリを検索し、関連付けられたすべての記事とそのタグやコメント、およびすべてのコメントのゲスト関連付けを一括読み込みします。

13.2 関連付けの一括読み込みで条件を指定する

Active Recordでは、joinsのように事前読み込みされた関連付けに対して条件を指定することができますが、joins という方法を使用することをお勧めします。

しかし、このようにせざるを得ない場合は、whereを通常どおりに使用することができます。

Article.includes(:comments).where(comments: { visible: true })

このコードは、LEFT OUTER JOINを含むクエリを1つ生成します。joinsメソッドを使用していたら、代りにINNER JOINを使用するクエリが生成されていたでしょう。

  SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)

where条件がない場合は、通常のクエリが2セット生成されます。

whereがこのように動作するのは、ハッシュを渡した場合だけです。SQL断片化 (fragmentation) を避けるためには、references を指定して強制的にテーブルをjoinする必要があります。

Article.includes(:comments).where("comments.visible = true").references(:comments)

このincludesクエリの場合、どの記事にもコメントがついていないので、すべての記事が読み込まれます。joins (INNER JOIN) を使用する場合、結合条件は必ずマッチ しなければならず 、それ以外の場合にはレコードは返されません。

もしjoinの一部として関連付けが一括読み込みされている場合、読み込まれたモデルの中にカスタマイズされたSelect句のフィールドが存在しなくなります。これは親レコード (または子レコード) の中で表示して良いかどうかが曖昧になってしまうためです。

14 スコープ

スコープを設定することで、関連オブジェクトやモデルへのメソッド呼び出しとして参照される、よく使用されるクエリを指定することができます。スコープでは、wherejoinsincludesなど、これまでに登場したすべてのメソッドを使用できます。どのスコープメソッドも、常にActiveRecord::Relationオブジェクトを返します。このオブジェクトに対して、別のスコープを含む他のメソッド呼び出しを行なうこともできます。

単純なスコープを設定するには、クラスの内部でscopeメソッドを使用し、スコープが呼び出されたときに実行して欲しいクエリをそこで渡します。

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

以下でもわかるように、スコープでのメソッドの設定は、クラスメソッドの定義と完全に同じ (というよりクラスメソッドの定義そのもの) です。どちらの形式を使用するかは好みの問題です。

class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end

スコープをスコープ内でチェイン (chain) させることもできます。

class Article < ApplicationRecord
  scope :published,               -> { where(published: true) }
  scope :published_and_commented, -> { published.where("comments_count > 0") }
end

このpublishedスコープを呼び出すには、クラスでこのスコープを呼び出します。

Article.published # => [published articles]

または、Articleオブジェクトからなる関連付けでこのスコープを呼び出します。

category = Category.first
category.articles.published # => [このカテゴリに属する、公開済みの記事]

14.1 引数を渡す

スコープには引数を渡すことができます。

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) }
end

引数付きスコープの呼び出しは、クラスメソッドの呼び出しと同様の方法で行います。

Article.created_before(Time.zone.now)

しかし、スコープに引数を渡す機能は、クラスメソッドによって提供される機能を単に複製したものです。

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time)
  end
end

したがって、スコープで引数を使用するのであれば、クラスメソッドとして定義する方が推奨されます。クラスメソッドにした場合でも、関連オブジェクトからアクセス可能です。

category.articles.created_before(time)

14.2 条件文を使う

スコープでは条件文を使うこともできます。

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end

以下の例からもわかるように、これはクラスメソッドのように振る舞います。

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end

ただし1つ注意点があります。それは条件文を評価した結果がfalseになった場合であっても、スコープは常にActiveRecord::Relationオブジェクトを返すという点です。クラスメソッドの場合はnilを返すので、この振る舞いが異なります。したがって、条件文を使ってクラスメソッドをチェインさせていて、かつ、条件文のいずれかがfalseを返す場合、NoMethodErrorを発生することがあります。

14.3 デフォルトスコープを適用する

あるスコープをモデルのすべてのクエリに適用したい場合、モデル自身の内部でdefault_scopeメソッドを使用することができます。

class Client < ApplicationRecord
  default_scope { where("removed_at IS NULL") }
end

このモデルに対してクエリが実行されたときのSQLクエリは以下のような感じになります。

SELECT * FROM clients WHERE removed_at IS NULL

デフォルトスコープの条件が複雑になるのであれば、スコープをクラスメソッドとして定義するのもひとつの手です。

class Client < ApplicationRecord
  def self.default_scope
    # ActiveRecord::Relationを返すようにする
  end
end

レコードを作成するときも、スコープの引数がHashとして与えられた場合はdefault_scopeが適用されます。ただし、レコードを更新する場合は適用されません。例:

class Client < ApplicationRecord
  default_scope { where(active: true) }
end

Client.new          # => #<Client id: nil, active: true>
Client.unscoped.new # => #<Client id: nil, active: nil>

引数にArrayが与えられた場合は、default_scopeクエリの引数はHashのデフォルト値に変換されない点に注意してください。例:

class Client < ApplicationRecord
  default_scope { where("active = ?", true) }
end

Client.new # => #<Client id: nil, active: nil>

14.4 スコープのマージ

where句と同様、スコープもAND条件でマージできます。

class User < ApplicationRecord
  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'

scopewhere条件を混用してマッチさせることができます。その結果生成される最終的な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で定義した条件が、scopewhereで定義した条件よりも先に評価されるという点です。

class User < ApplicationRecord
  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" = 'pending' AND "users"."state" = 'active'

User.where(state: 'inactive')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'

上の例でわかるように、default_scopeの条件が、scopewhereの条件よりも先に評価されています。

14.5 すべてのスコープを削除する

何らかの理由でスコープをすべて解除したい場合はunscopedメソッドを使用できます。このメソッドは、モデルでdefault_scopeが指定されているが、それを適用したくないクエリがある場合に特に便利です。

Client.unscoped.load

このメソッドはスコープをすべて解除し、テーブルに対して通常の (スコープなしの) クエリを実行するようにします。

Client.unscoped.all
# SELECT "clients".* FROM "clients"

Client.where(published: false).unscoped.all
# SELECT "clients".* FROM "clients"

unscoped はブロックも受け取れます。

Client.unscoped {
  Client.created_before(Time.zone.now)
}

15 動的検索

Active Recordは、テーブルに定義されたすべてのフィールド (属性とも呼ばれます) に対して自動的に検索メソッドを提供します。たとえば、Clientモデルにfirst_nameというフィールドがあると、find_by_first_nameというメソッドがActive Recordによって自動的に作成されます。Clientモデルにlockedというフィールドがあれば、find_by_lockedというメソッドを使用できます。

この動的検索メソッドの末尾にClient.find_by_name!("Ryan")のように感嘆符 (!) を追加すると、該当するレコードがない場合にActiveRecord::RecordNotFoundエラーが発生します。

nameとlockedの両方を検索したいのであれば、2つのフィールド名をandでつなぐだけでメソッドを利用できます。たとえば、Client.find_by_first_name_and_locked("Ryan", true)のように書くことができます。

16 Enums

enumマクロは整数のカラムを設定可能な値の集合にマッピングします。

class Book < ApplicationRecord
  enum availability: [:available, :unavailable]
end

これは対応するスコープを自動的に作成します。状態の遷移や現在の状態の問い合わせ用のメソッドも追加されます。

# 下の両方の例で、利用可能な本を問い合わせている
Book.available
# または
Book.where(availability: :available)

book = Book.new(availability: :available)
book.available?   # => true
book.unavailable! # => true
book.available?   # => false

enumの詳細な仕様については、 Rails APIを参照してください。

17 メソッドチェインを理解する

Active Record パターンには メソッドチェイン (Method chaining - Wikipedia) が実装されています。これにより、複数のActive Recordメソッドをシンプルな方法で次々に適用することができます。

文中でメソッドチェインができるのは、その前のメソッドがActiveRecord::Relation (allwherejoinsなど) をひとつ返す場合です。文の末尾には、単一のオブジェクトを返すメソッド (単一のオブジェクトを取り出すを参照) をひとつ置かなければなりません。

いくつか例をご紹介します。本ガイドでは一部の例のみをご紹介し、すべての例を網羅することはしません。Active Recordメソッドが呼び出されると、クエリはその時点ではすぐに生成されず、データベースに送信されます。クエリは、データが実際に必要になった時点で初めて生成されます。以下の例では、いずれも単一のクエリを生成します。

17.1 複数のテーブルからのデータをフィルタして取得する

Person
  .select('people.id, people.name, comments.text')
  .joins(:comments)
  .where('comments.created_at > ?', 1.week.ago)

結果は次のようなものになります。

SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
  ON comments.person_id = people.id
WHERE comments.created_at > '2015-01-01'

17.2 複数のテーブルから特定のデータを取得する

Person
  .select('people.id, people.name, companies.name')
  .joins(:company)
  .find_by('people.name' => 'John') # 名を指定

上のコードから以下が生成されます。

SELECT people.id, people.name, companies.name
FROM people
INNER JOIN companies
  ON companies.person_id = people.id
WHERE people.name = 'John'
LIMIT 1

ひとつのクエリが複数のレコードとマッチする場合、find_byは「最初」の結果だけを返し、他は返しません (上のLIMIT 1 文を参照)。

18 新しいオブジェクトを検索またはビルドする

レコードを検索し、レコードがなければ作成する、というのはよくある一連の流れです。find_or_create_byおよびfind_or_create_by!メソッドを使用すればこれらを一度に行なうことができます。

18.1 find_or_create_by

find_or_create_byメソッドは、指定された属性を持つレコードが存在するかどうかをチェックします。レコードがない場合はcreateが呼び出されます。以下の例を見てみましょう。

'Andy'という名前のクライアントを探し、いなければ作成したいとします。これを行なうには以下を実行します。

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という名前のクライアントがなかったのでレコードを作成して返しました。

createなどと同様、検証にパスするかどうかによって、新しいレコードがデータベースに保存されていないことがあるかもしれません。

今度は、新しいレコードを作成するときに'locked'属性をfalseに設定したいが、それをクエリに含めたくないとします。そこで、"Andy"という名前のクライアントを検索するか、その名前のクライアントがいない場合は"Andy"というクライアントを作成してロックを外すことにします。

これは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

このブロックは、クライアントが作成されるときにだけ実行されます。このコードを再度実行すると、このブロックは実行されません。

18.2 find_or_create_by!

find_or_create_by!を使用すると、新しいレコードが無効な場合に例外を発生することもできます。検証 (validation) については本ガイドでは解説していませんが、たとえば

validates :orders_count, presence: true

上をClientモデルに追加したとします。orders_countを指定しないで新しいClientモデルを作成しようとすると、レコードは無効になって例外が発生します。

Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank

18.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

19 SQLで検索する

独自のSQLを使用してレコードを検索したい場合、find_by_sqlメソッドを使用できます。このfind_by_sqlメソッドは、オブジェクトの配列を1つ返します。クエリがレコードを1つしか返さなかった場合にも配列が返されますのでご注意ください。たとえば、以下のクエリを実行したとします。

Client.find_by_sql("SELECT * FROM clients
  INNER JOIN orders ON clients.id = orders.client_id
  ORDER BY clients.created_at desc")
# =>  [
#   #<Client id: 1, first_name: "Lucas" >,
#   #<Client id: 2, first_name: "Jan" >,
#   ...
# ]

find_by_sqlは、カスタマイズしたデータベース呼び出しを簡単な方法で提供し、インスタンス化されたオブジェクトを返します。

19.1 select_all

find_by_sqlconnection#select_allと深い関係があります。select_allfind_by_sqlと同様、カスタムSQLを使用してデータベースからオブジェクトを取り出しますが、取り出したオブジェクトのインスタンス化を行わない点が異なります。このメソッドはActiveRecord::Resultクラスのインスタンスを1つ返します。このオブジェクトでto_hashを呼ぶと、各レコードに対応するハッシュを含む配列を1つ返します。

Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash
# => [
#   {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
#   {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
# ]

19.2 pluck

pluckは、1つのモデルで使用されているテーブルからカラム (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 }
# または
Client.select(:id).map(&:id)
# または
Client.select(:id, :name).map { |c| [c.id, c.name] }

上は以下に置き換えられます。

Client.pluck(:id)
# または
Client.pluck(:id, :name)

selectと異なり、pluckはデータベースから受け取った結果を直接Rubyの配列に変換してくれます。そのためのActiveRecordオブジェクトを事前に構成しておく必要はありません。従って、このメソッドは大規模なクエリや使用頻度の高いクエリで使用するとパフォーマンスが向上します。ただし、オーバーライドを行なうモデルメソッドは使用できません。以下に例を示します。

class Client < ApplicationRecord
  def name
    "私は#{super}"
  end
end

Client.select(:name).map &:name
# => ["私はDavid", "私はJeremy", "私はJose"]

Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]

さらにpluckは、selectなどのRelationスコープと異なり、クエリを直接トリガするので、その後ろに他のスコープをチェインすることはできません。ただし、構成済みのスコープをpluckの前に置くことはできます。

Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

Client.limit(1).pluck(:name)
# => ["David"]

19.3 ids

idsは、テーブルの主キーを使用するリレーションのIDをすべて取り出すのに使用できます。

Person.ids
# SELECT id FROM people
class Person < ApplicationRecord
  self.primary_key = "person_id"
end

Person.ids
# SELECT person_id FROM people

20 オブジェクトの存在チェック

オブジェクトが存在するかどうかをチェックしたい時はexists?メソッドを使います。このメソッドは、findと同様のクエリを使用してデータベースにクエリを送信しますが、オブジェクトのコレクションの代わりにtrueまたはfalseを返します。

Client.exists?(1)

exists?は複数の値を引数に取ることができます。ただし、それらの値のうち1つでも存在していれば、他の値が存在していなくてもtrueを返します。

Client.exists?(id: [1,2,3])
# または
Client.exists?(name: ['John', 'Sergei'])

exists?メソッドは、引数なしでモデルやリレーションに使用することもできます。

Client.where(first_name: 'Ryan').exists?

上の例では、first_nameが'Ryan'のクライアントが1人でもいればtrueを返し、それ以外の場合はfalseを返します。

Client.exists?

上の例では、Clientテーブルが空ならfalseを返し、それ以外の場合はtrueを返します。

モデルやリレーションでの存在チェックにはany?many?も使用できます。

# via a model
Article.any?
Article.many?

# 名前付きスコープを経由
Article.recent.any?
Article.recent.many?

# リレーション経由
Article.where(published: true).any?
Article.where(published: true).many?

# 関連付け経由
Article.first.categories.any?
Article.first.categories.many?

21 計算

このセクションでは冒頭でcountメソッドを例に取って説明していますが、ここで説明されているオプションは以下のすべてのサブセクションにも該当します。

あらゆる計算メソッドは、モデルに対して直接実行されます。

Client.count
# SELECT count(*) AS count_all FROM clients

リレーションに対しても直接実行されます。

Client.where(first_name: 'Ryan').count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')

この他にも、リレーションに対してさまざまな検索メソッドを使用して複雑な計算を行なうことができます。

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 = clients.id WHERE
  (clients.first_name = 'Ryan' AND orders.status = 'received')

21.1 個数を数える

モデルのテーブルに含まれるレコードの個数を数えるにはClient.countを使用できます。返されるのはレコードの個数です。特定の年齢のクライアントの数を数えるのであれば、Client.count(:age)とします

オプションについては、1つ上の計算セクションを参照してください。

21.2 平均

テーブルに含まれる特定の数値の平均を得るには、そのテーブルを持つクラスに対してaverageメソッドを呼び出します。このメソッド呼び出しは以下のようなものになります。

Client.average("orders_count")

返される値は、そのフィールドの平均値です。通常3.14159265のような浮動小数点になります。

オプションについては、1つ上の計算セクションを参照してください。

21.3 最小値

テーブルに含まれるフィールドの最小値を得るには、そのテーブルを持つクラスに対してminimumメソッドを呼び出します。このメソッド呼び出しは以下のようなものになります。

Client.minimum("age")

オプションについては、1つ上の計算セクションを参照してください。

21.4 最大値

テーブルに含まれるフィールドの最大値を得るには、そのテーブルを持つクラスに対してmaximumメソッドを呼び出します。このメソッド呼び出しは以下のようなものになります。

Client.maximum("age")

オプションについては、1つ上の計算セクションを参照してください。

21.5 合計

テーブルに含まれるフィールドのすべてのレコードにおける合計を得るには、そのテーブルを持つクラスに対してsumメソッドを呼び出します。このメソッド呼び出しは以下のようなものになります。

Client.sum("orders_count")

オプションについては、1つ上の計算セクションを参照してください。

22 EXPLAINを実行する

リレーションによってトリガされるクエリでEXPLAINを実行することができます。以下に例を示します。

User.where(id: 1).joins(:articles).explain

以下のような結果が生成されます。

EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+----------+-------+---------------+
| id | select_type | table    | type  | possible_keys |
+----+-------------+----------+-------+---------------+
|  1 | SIMPLE      | users    | const | PRIMARY       |
|  1 | SIMPLE      | articles | ALL   | NULL          |
+----+-------------+----------+-------+---------------+
+---------+---------+-------+------+-------------+
| key     | key_len | ref   | rows | Extra       |
+---------+---------+-------+------+-------------+
| PRIMARY | 4       | const |    1 |             |
| NULL    | NULL    | NULL  |    1 | Using where |
+---------+---------+-------+------+-------------+

2 rows in set (0.00 sec)

上の結果はMySQLの場合です。

Active Recordは、データベースシェルを模したデータをある程度整形して出力します。PostgreSQLアダプタで同じクエリを実行すると、今度は以下のような結果が得られます。

EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."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: (articles.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 articles  (cost=0.00..28.88 rows=8 width=4)
         Filter: (articles.user_id = 1)
(6 rows)

一括読み込みを使用していると、内部で複数のクエリがトリガされることがあり、一部のクエリではその前の結果を必要とすることがあります。このため、explainはこのクエリを実際に実行し、それからクエリプランを要求します。以下に例を示します。

User.where(id: 1).includes(:articles).explain

以下の結果を生成します。

EXPLAIN for: SELECT `users`.* FROM `users`  WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+
| id | select_type | table | type  | possible_keys |
+----+-------------+-------+-------+---------------+
|  1 | SIMPLE      | users | const | PRIMARY       |
+----+-------------+-------+-------+---------------+
+---------+---------+-------+------+-------+
| key     | key_len | ref   | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4       | const |    1 |       |
+---------+---------+-------+------+-------+

1 row in set (0.00 sec)

EXPLAIN for: SELECT `articles`.* FROM `articles`  WHERE `articles`.`user_id` IN (1)
+----+-------------+----------+------+---------------+
| id | select_type | table    | type | possible_keys |
+----+-------------+----------+------+---------------+
|  1 | SIMPLE      | articles | ALL  | NULL          |
+----+-------------+----------+------+---------------+
+------+---------+------+------+-------------+
| key  | key_len | ref  | rows | Extra       |
+------+---------+------+------+-------------+
| NULL | NULL    | NULL |    1 | Using where |
+------+---------+------+------+-------------+


1 row in set (0.00 sec)

上の結果はMySQLとMariaDBの場合です。

22.1 EXPLAINの出力結果を解釈する

EXPLAINの出力を解釈することは、本ガイドの範疇を超えます。 以下の情報を参考にしてください。

支援・協賛

Railsガイドは下記のサポーターから継続的な支援を受けています。