その他
    ホーム 技術発信 DoRuby 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上でやるのが速いかは場合によったりもしますし、違いを知って使い分けていければいいなと思っています。

    参考:

    記事を共有
    モバイルバージョンを終了