ホーム DoRuby ActiveRecord::Relationとは一体なんなのか
ActiveRecord::Relationとは一体なんなのか
 

ActiveRecord::Relationとは一体なんなのか

この記事はアピリッツの技術ブログ「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上でやるのが速いかは場合によったりもしますし、違いを知って使い分けていければいいなと思っています。

参考:

記事を共有

最近人気な記事