目次
この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
ActiveRecord::Relation まわりのことを、ソースコードを読んで調べてまとめました。
はじめに
Ruby on Railsを使い始めて2ヶ月と少しの初心者ですが、最近はなんとなくわかったような気になってrailsを使っています。
つまりわかっていないということなので、これもいい機会だと思って ActiveRecord::Relation について調べてみました(ソースコードを読んでみました)。
この記事の内容はざっくりいうと ActiveRecord::Relation とは一体何で、何をしているのか…という話(の一部分)です。
特に、どういうタイミングでどういったSQLクエリが発行されるか、といったことに注目しています。
これを決める前に最初に持っていた疑問は
- 関連付けで作られたメソッドは一体何を返しているの?
- eachって一体何に対してeachを使っているの?
- find はArrayのようにコードブロックもとれるらしいけれど、一体どうやってるの?
- どのメソッドを使うとどういうクエリが何回発行されるの?
などでした(記事中で解決するものも、しないものもあります)。
以下では記事を書いた時点でのrailsソースコード(5.1.0 相当)を参考にしていますが、おそらく5系統の中ではそれほど違うことはないかと思います。
そもそもActiveRecord::Relationとは?
ActiveRecord::Relationは
- クエリを生成するための条件を持っておいて、必要に応じて適切なSQLクエリを生成・発行してくれる
- その結果からオブジェクトを作って返したり保持したりしてくれる
感じのものです。つい最近知ったのですが、こういったものをO/Rマッパーというそうです。
ActiveRecord::Relationがどこで出てくるかというと、たとえばUserなるモデルがあったとすると User.all
や User.where(name: "hoge")
などで返ってくるものが ActiveRecord::Relation のインスタンスです。
また、has_many関連で作られたメソッドは ActiveRecord::CollectionProxy のインスタンスを返しますが、この ActiveRecord::CollectionProxy は ActiveRecord::Relationを継承しています。
初心者が ActiveRecord::Relation について知るには、ひとまず以下のインスタンス変数に注目するのがよさそうです。
@values
: SQLクエリを作るための条件@loaded
: SQLクエリを発行し、 オブジェクトを取得したことがあるかのフラグ- (ソースコード中では loaded? という名前で参照されています)
@records
: 条件に合うオブジェクトの配列
@records
にアクセスするための records というメソッドがあります。
これは、 loaded?
がfalseのとき(=まだオブジェクトを取得していないとき)はクエリを発行して@records
を取得して返し、loaded?
が trueのとき(=すでにオブジェクトを取得しているとき)は @records
をそのまま返します、
# activerecord/lib/active_record/relation.rb
def records # :nodoc:
load
@records
end
def load(&block)
exec_queries(&block) unless loaded?
self
end
loadする前は @records
がないので、だいたい loaded?のtrue/false = @records
の有無です。
各メソッドはこの records を使ったり使わなかったりします。知る限りではだいたい3種類です。
- recordsを使うもの
- recordsを使わないもの
- その他
records を使うメソッド
recordsを使うメソッドは、loaded? がtrueならクエリを発行しないメソッドでもあります。
records に delegateされているメソッド
ActiveRecord::Relation のいくつかのメソッドは records に delegate されています。
# activerecord/lib/active_record/relation/delegation.rb
delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :index, to: :records
つまり records(Array) に対してメソッドを呼び出すもので、すでに @records
があればSQLクエリを発行しません。
また、ActiveRecord::Relation は Enumerableをインクルードしています。
delegateされていなくても、Enumerableのメソッド (例えば map, collect など)は each を使って定義されているので、他のメソッドと同様 @records
のメソッドを呼び出すことになります。
5.0.3では map,collect などもdelegate されているようですが、recordsに対してArrayのメソッドが呼ばれることに変わりはありません。
pluck
pluck は各レコードを丸ごとオブジェクトとしてとってくるのではなく、引数で指定したカラムのみの配列で返すメソッドです。
pluck はdelegateされているわけではないですが、 @records
があればそちらからとってきます。
# activerecord/lib/active_record/relation/calculations.rb
def pluck(*column_names)
if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
return records.pluck(*column_names)
end
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
relation.select_values = column_names.map { |cn|
@klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
}
result = klass.connection.select_all(relation.arel, nil, bound_attributes)
result.cast_values(klass.attribute_types)
end
end
size
size は、 @records
があればArray#lengthでサイズを取得します。@records
がない場合DB内でサイズを計算してしまうので、 @records
は更新されません。
# activerecord/lib/active_record/relation.rb def size loaded? ? @records.length : count(:all) end
recordsを使わないメソッド
@records
があってもそれを使わずに、SQLクエリを使って計算を行うメソッドがあります。
rails ガイドで列挙されているこのあたりがそうです。
https://railsguides.jp/active_record_querying.html#%E8%A8%88%E7%AE%97
- count
- sum
- average
- minimum
- maximum
この5つは activerecord/lib/active_record/relation/calculations.rb で定義されています。
count, sum は Array にもあるメソッドなのでややこしいですが、 ActiveRecord::Relation のcountやsumはコードブロックを無視します。
Arrayのcountやsumを使いたい時はrecords、to_aなどでArrayに変換する必要があります。
その他のややこしいメソッド
教えてもらうまで全く知らなかったのですが、コードブロックを渡すかどうかで
SQLクエリとしてそれを処理するかArrayとして処理するかが変わるメソッドがあるそうです。
find
findは引数を渡すかコードブロックを渡すかで挙動が変わります。
引数を渡したときは引数を渡したときはSQLを発行し、idが引数と一致するものをひとつ探してきます。
コードブロックを渡したときはEnumerable(というかArray)のfindが呼ばれます。
# activerecord/lib/active_record/relation/finder_methods.rb
def find(*args)
return super if block_given?
find_with_ids(*args)
end
select
selectもfindと同じように、引数を渡すかコードブロックを渡すか挙動が変わります。
引数(カラム名)を渡した時のselectは、引数のカラムの値だけを取ってきて、その値のみが入ったオブジェクトを返すものです。
( SQLクエリは “SELECT `args` FROM table_name …” のようになります)。
コードブロックを渡した時のselectは、Arrayのselectです。コードブロックに渡したときtrueを返すものの配列を返します。
これはそもそも用途が違うので、どちらを使うか迷うことはなさそうですが…
# activerecord/lib/active_record/relation/finder_methods.rb
def select(*fields)
if block_given?
if fields.any?
raise ArgumentError, "`select' with block doesn't take arguments."
end
return super()
end
raise ArgumentError, "Call `select' with at least one field" if fields.empty?
spawn._select!(*fields)
end
まとめ
ここまで出てきたメソッドをもう一度おさらいしておきます。
- 条件にあう配列 records がすでにあればそれを使うもの
- each, Enumerableのメソッド
- [], length, uniq, sample, reverse, compact などのdelegateされているメソッド
- pluck
- size
- recordsがあってもなくてもクエリで計算を行うもの
- count
- sum
- minimum
- average
- maximum
- その他
- find
- select
ここで列挙したのは個人的に使うメソッドだけなのですが、同じように見ていけば他のメソッドもどうなっているかわかるはずです。
なんとなくで使えてしまうのでなんとなく使ってしまいがちなActiveRecordですが、なんとなくでも何が起こっているのか知っておくと後々役に立ちそうです。
findの使い分けやcount,size,lengthの使い分けはDBでやるのが速いか、rails上でやるのが速いかは場合によったりもしますし、違いを知って使い分けていければいいなと思っています。
参考:
- Ruby on Rails ガイド https://railsguides.jp/
- railsソースコード https://github.com/rails/rails