Ashigaruコンピューター道

ソフトウェアの話とか、キャリアの話とか

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でも拾ってコードリーディングしてみるかな。