調べた問題
ActiveRecordでCompanyモデルに紐づくUserという物があったときに、関連のあるUserを作成するときにbuildを使って以下のようにする。その際、まだDBには保存されていないUserが次の検索ででくるか気になったので実験しつつ、ActiveRecordのソースを調べてみた。Company.users.build(name: 'xxxxx')
調査
railsのソースコードリーディングについてここ参考にさせてもらった。AZS
上を参考にしつつ、railsのコードを落としてコードリーディング用のプロジェクトを作成した後、モデルを作成して、DBにテーブルを作る。
そしてCompanyにCompanyとUserの関連を持たせるのと、コードを追うためのメソッドを追加。
実行して追いかけてみる。buildはcollection_proxy.rbのbuildメソッドが呼ばれているもよう。
@associationの実態は以下のようActiveRecord::Associations::HasManyAssociationというクラスのインスタンスでフィールドにCompanyとUserに関する情報をいろいろ持っている。
Userのインスタンス化自体は、このパターンだとObjectのnewが使われていた。
こので戻ってきたメソッドないでは、Userがインスタンス化されている。
before_addとかafter_addはここで呼ばれているのか、へー
targetというArrayにこのレコードを入れているが、最終的にこれがCompanyと関連をもって保存されるのかな?
この次はもう呼び出し元へ戻ってきて、usersの中身は入ってた。
whereの実態はココらへん
その後 self.build_users へ戻ってきてしまったので、com.users.where(name: 'test_company')の段階では検索の条件などを組み立てるだけで、実際の値の検索はしていないようなので(遅延評価ってやつかな?)ちょっとコードを変えて再実行。
以下のように最後にwhereで組み立てたusersをカウントすることによって、どこの値をみてるかわかるはず。
追ってくとこんなところがあって、SQLを組み立ててるのでDBに取りに行く模様
結局SQLが発行されてDBの値が検索されてそれが検索される。
実際にその値が必要になった場合に、「DBから」検索されるので、紐付いているオブジェクトは検索結果には入ってこない。
上を参考にしつつ、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でも拾ってコードリーディングしてみるかな。