2017年6月24日土曜日

Ruby on Rails Active Recordのソースコードリーディング

調べた問題

ActiveRecordでCompanyモデルに紐づくUserという物があったときに、関連のあるUserを作成するときにbuildを使って以下のようにする。その際、まだDBには保存されていないUserが次の検索ででくるか気になったので実験しつつ、ActiveRecordのソースを調べてみた。

Company.users.build(name: 'xxxxx')


調査

railsのソースコードリーディングについてここ参考にさせてもらった。AZS

上を参考にしつつ、railsのコードを落としてコードリーディング用のプロジェクトを作成した後、モデルを作成して、DBにテーブルを作る。

rails g model Company name:string
rails g model User company_id:integer name:string

bundle exec rails db:migrate RAILS_ENV=development
== 20170617020543 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0027s
== 20170617020543 CreateUsers: migrated (0.0028s) =============================

== 20170617020644 CreateCompanies: migrating ==================================
-- create_table(:companies)
   -> 0.0010s
== 20170617020644 CreateCompanies: migrated (0.0015s) =========================

そしてCompanyにCompanyとUserの関連を持たせるのと、コードを追うためのメソッドを追加。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    com.users.where(name: 'test_company')
  end
end

実行して追いかけてみる。buildはcollection_proxy.rbのbuildメソッドが呼ばれているもよう。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_proxy.rb @ line 316 ActiveRecord::Associations::CollectionProxy#build:

    315: def build(attributes = {}, &block)
 => 316:   @association.build(attributes, &block)
    317: end

 @associationの実態は以下のようActiveRecord::Associations::HasManyAssociationというクラスのインスタンスでフィールドにCompanyとUserに関する情報をいろいろ持っている。


[1] pry(#<User::ActiveRecord_Associations_CollectionProxy>)> @association
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = ?  [["company_id", 1]]
=> #<ActiveRecord::Associations::HasManyAssociation:0x007f752027be50
 @association_scope=nil,
 @inversed=false,
 @loaded=false,
 @owner=#<Company:0x007f753ec2a668 id: 1, name: "test_company", created_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00, updated_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00>,
 @proxy=[],
 @reflection=
  #<ActiveRecord::Reflection::HasManyReflection:0x007f753f044988
   @active_record=Company(id: integer, name: string, created_at: datetime, updated_at: datetime),
   @active_record_primary_key="id",
   @association_scope_cache=
    {true=>
      #<ActiveRecord::StatementCache:0x007f753efcb060
       @bind_map=
        #<ActiveRecord::StatementCache::BindMap:0x007f753efcb5d8
         @bound_attributes=
          [#<ActiveRecord::Relation::QueryAttribute:0x007f753efc5b10
            @name="company_id",
            @original_attribute=nil,
            @type=#<ActiveModel::Type::Integer:0x007f753efa4a00 @limit=nil, @precision=nil, @range=-2147483648...2147483648, @scale=nil>,
            @value=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>,
            @value_before_type_cast=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>>],
         @indexes=[0]>,
       @query_builder=#<ActiveRecord::StatementCache::Query:0x007f753efcb088 @sql="SELECT \"users\".* FROM \"users\" WHERE \"users\".\"company_id\" = ?">>},
   @automatic_inverse_of=false,
   @class_name="User",
   @constructable=true,
   @foreign_key="company_id",
   @foreign_type="users_type",
   @klass=User(id: integer, company_id: integer, name: string, created_at: datetime, updated_at: datetime),
   @name=:users,
   @options={},
   @plural_name="users",
   @scope=nil,
   @scope_lock=#<Thread::Mutex:0x007f753f044618>,
   @type=nil>,
 @stale_state=nil,
 @target=[]>

Userのインスタンス化自体は、このパターンだとObjectのnewが使われていた。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/inheritance.rb @ line 65 ActiveRecord::Inheritance::ClassMethods#new:

    48: def new(*args, &block)
    49:   if abstract_class? || self == Base
    50:     raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
    51:   end
    52:
    53:   attrs = args.first
    54:   if has_attribute?(inheritance_column)
    55:     subclass = subclass_from_attributes(attrs)
    56:
    57:     if subclass.nil? && base_class == self
    58:       subclass = subclass_from_attributes(column_defaults)
    59:     end
    60:   end
    61:
    62:   if subclass && subclass != self
    63:     subclass.new(*args, &block)
    64:   else
 => 65:     super
    66:   end
    67: end

[9] pry(User)> self.parent
=> Object


こので戻ってきたメソッドないでは、Userがインスタンス化されている。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 281 ActiveRecord::Associations::CollectionAssociation#add_to_target:

    277: def add_to_target(record, skip_callbacks = false, &block)
    278:   if association_scope.distinct_value
    279:     index = @target.index(record)
    280:   end
 => 281:   replace_on_target(record, index, skip_callbacks, &block)
    282: end

[11] pry(#<ActiveRecord::Associations::HasManyAssociation>)> record
=> #<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>


before_addとかafter_addはここで呼ばれているのか、へー


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 441 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
 => 441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
    452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

targetというArrayにこのレコードを入れているが、最終的にこれがCompanyと関連をもって保存されるのかな?


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 452 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
    441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
 => 452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

[17] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target
=> []
[19] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target.class
=> Array

この次はもう呼び出し元へ戻ってきて、usersの中身は入ってた。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 9 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
 =>  9:   com.users.where(name: 'test_company')
    10: end

[24] pry(Company)> com.users
=> [#<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>]

whereの実態はココらへん

From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 600 ActiveRecord::QueryMethods#where:

    599: def where(opts = :chain, *rest)
 => 600:   if :chain == opts
    601:     WhereChain.new(spawn)
    602:   elsif opts.blank?
    603:     self
    604:   else
    605:     spawn.where!(opts, *rest)
    606:   end
    607: end



From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 610 ActiveRecord::QueryMethods#where!:

    609: def where!(opts, *rest) # :nodoc:
 => 610:   opts = sanitize_forbidden_attributes(opts)
    611:   references!(PredicateBuilder.references(opts)) if Hash === opts
    612:   self.where_clause += where_clause_factory.build(opts, rest)
    613:   self
    614: end

その後 self.build_users へ戻ってきてしまったので、com.users.where(name: 'test_company')の段階では検索の条件などを組み立てるだけで、実際の値の検索はしていないようなので(遅延評価ってやつかな?)ちょっとコードを変えて再実行。

以下のように最後にwhereで組み立てたusersをカウントすることによって、どこの値をみてるかわかるはず。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    users = com.users.where(name: 'test_company')
    _count_users = users.count
  end
end
~



追ってくとこんなところがあって、SQLを組み立ててるのでDBに取りに行く模様


From: /home/ishioka/repos/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @ line 34 ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all:

    31: def select_all(arel, name = nil, binds = [], preparable: nil)
    32:   arel, binds = binds_from_relation arel, binds
    33:   sql = to_sql(arel, binds)
 => 34:   if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
    35:     preparable = false
    36:   else
    37:     preparable = visitor.preparable
    38:   end
    39:   if prepared_statements && preparable
    40:     select_prepared(sql, name, binds)
    41:   else
    42:     select(sql, name, binds)
    43:   end
    44: end

[2] pry(#<ActiveRecord::ConnectionAdapters::SQLite3Adapter>)> sql
=> "SELECT COUNT(*) FROM \"users\" WHERE \"users\".\"company_id\" = ? AND \"users\".\"name\" = ?"


結局SQLが発行されてDBの値が検索されてそれが検索される。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 10 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
     9:   users = com.users.where(name: 'test_company')
 => 10:   _count_users = users.count
    11: end

[1] pry(Company)> n
   (0.8ms)  SELECT COUNT(*) FROM "users" WHERE "users"."company_id" = ? AND "users"."name" = ?  [["company_id", 1], ["name", "test_company"]]

From: /home/ishioka/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/pry-0.10.4/lib/pry/pry_instance.rb @ line 356 Pry#evaluate_ruby:

    351: def evaluate_ruby(code)
    352:   inject_sticky_locals!
    353:   exec_hook :before_eval, code, self
    354:
    355:   result = current_binding.eval(code, Pry.eval_path, Pry.current_line)
 => 356:   set_last_result(result, code)
    357: ensure
    358:   update_input_history(code)
    359:   exec_hook :after_eval, result, self
    360: end

[1] pry(#<Pry>)> c
=> 0


結論

whereでは検索条件が作成されるだけであって、まだ検索はされない。
実際にその値が必要になった場合に、「DBから」検索されるので、紐付いているオブジェクトは検索結果には入ってこない。

最後に

ActiveRecordのコードを少し追っかけてみたが、結構複雑で呼び出し順や、各オブジェクトの関係などすぐには理解できなそう。
ただすごい参考になりそう(rubyの使い方、設計思想など)なので、issueでも拾ってコードリーディングしてみるかな。

0 件のコメント:

コメントを投稿