Ruby on Railsを使ったアプリケーション開発で、DBにMySQLを使いカラムの型定義がIntegerの時にデフォルト値が文字列で設定される現象を観測しました。 デフォルト値だけ見て型をみないことに疑問を感じました。 そこでActiveRecordのソースコードを読みました。
準備
規模の大きなRubyのソースコードを読むときは、手元にソースコードを持ってきます。
git clone git@github.com:rails/rails.git
次のメリットがあります。
- RubyMineを使って定義を探せる
- 実際に動かせる
- 修正して動きを変更できる
入り口を探す
ActiveRecordはかなり大きなモジュールです。どこから読んだらいいのか目星をつけたいです。 今回はオブジェクトの初期化に関するソースコードが読みたいです。 というわけで、適当なクラスを初期化します。
~ bundle exec irb -r active_record irb(main):001:0> class Post < ActiveRecord::Base; end => nil irb(main):002:0> Post.new /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb:208:in `retrieve_connection': No connection pool for 'ActiveRecord::Base' found. (ActiveRecord::ConnectionNotEstablished) from /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_handling.rb:309:in `retrieve_connection' from /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_handling.rb:265:in `connection' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:554:in `load_schema!' from /Users/shigerunakajima/rails/activerecord/lib/active_record/attributes.rb:264:in `load_schema!' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:540:in `block in load_schema' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:537:in `synchronize' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:537:in `load_schema' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:403:in `attribute_types' from /Users/shigerunakajima/rails/activerecord/lib/active_record/attribute_methods.rb:187:in `_has_attribute?' from /Users/shigerunakajima/rails/activerecord/lib/active_record/inheritance.rb:59:in `new' from (irb):2:in `<main>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/bin/irb:23:in `load' from /Users/shigerunakajima/.rbenv/versions/3.0.1/bin/irb:23:in `<top (required)>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `load' from /Users/shigerunakajima/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `kernel_load' ... 13 levels...
activerecord/lib/active_record/attribute_methods.rb:187:in `_has_attribute?'
が気になります。
def _has_attribute?(attr_name) # :nodoc: attribute_types.key?(attr_name) end
いまいちでした。 呼び出し元をみます。
def new(attributes = nil, &block) if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end if _has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) if subclass.nil? && scope_attributes = current_scope&.scope_for_create subclass = subclass_from_attributes(scope_attributes) end if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end end if subclass && subclass != self subclass.new(attributes, &block) else super end end
newメソッドが定義されています。 いかにも入り口という感じです。
newメソッドを読む
newメソッドの中身をみるとsubclassをさがしています。 subclassはなんでしょうか? subclassを取得するのに使っているsubclass_from_attributesメソッドを覗いてみます。
def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym] if subclass_name.present? find_sti_class(subclass_name) end end end
find_sti_classが気になります。 おそらくSTIを使っている場合にインスタンス化するクラスを変更する処理でしょう。
そう仮定して、newメソッドを見直すと、subclassが見つからなかったときはsuperメソッドを読んでいます。
superは何か?
ソースコードから、自信を持ってsuperが何かを読み取るのは難しいです。 newメソッドをいきなりsuperを呼ぶように変更します。
def new(attributes = nil, &block) super end
もう一回初期化してみましょう。
~ bundle exec irb -r active_record irb(main):001:0> class Post < ActiveRecord::Base; end => nil irb(main):002:0> Post.new /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb:208:in `retrieve_connection': No connection pool for 'ActiveRecord::Base' found. (ActiveRecord::ConnectionNotEstablished) from /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_handling.rb:309:in `retrieve_connection' from /Users/shigerunakajima/rails/activerecord/lib/active_record/connection_handling.rb:265:in `connection' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:554:in `load_schema!' from /Users/shigerunakajima/rails/activerecord/lib/active_record/attributes.rb:264:in `load_schema!' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:540:in `block in load_schema' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:537:in `synchronize' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:537:in `load_schema' from /Users/shigerunakajima/rails/activerecord/lib/active_record/model_schema.rb:461:in `_default_attributes' from /Users/shigerunakajima/rails/activerecord/lib/active_record/core.rb:575:in `initialize' from /Users/shigerunakajima/rails/activerecord/lib/active_record/inheritance.rb:55:in `new' from /Users/shigerunakajima/rails/activerecord/lib/active_record/inheritance.rb:55:in `new' from (irb):2:in `<main>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/bin/irb:23:in `load' from /Users/shigerunakajima/.rbenv/versions/3.0.1/bin/irb:23:in `<top (required)>' from /Users/shigerunakajima/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `load' ... 14 levels...
activerecord/lib/active_record/core.rb:575:in `initialize'
が、気になります。coreクラスのinitializeってめっちゃそれっぽい名前です。
def initialize(attributes = nil) @new_record = true @attributes = self.class._default_attributes.deep_dup init_internals initialize_internals_callback assign_attributes(attributes) if attributes yield self if block_given? _run_initialize_callbacks end
まさにinitializeって感じの処理です。
self.class._default_attributes.deep_dup
めちゃめちゃ怪しい感じの名前が見つかりました。
_default_attributes
def _default_attributes # :nodoc: load_schema @default_attributes ||= ActiveModel::AttributeSet.new({}) end
全然それっぽくないんですよ。 空のオブジェクトをセットするだけに見えます。
load_schemaは怪しいので追いかけてみましょう。 スタックトレースを見ると、load_schemaはその先でload_schema!を呼んでいます。
def load_schema! unless table_name raise ActiveRecord::TableNotSpecified, "#{self} has no table configured. Set one with #{self}.table_name=" end columns_hash = connection.schema_cache.columns_hash(table_name) columns_hash = columns_hash.except(*ignored_columns) unless ignored_columns.empty? @columns_hash = columns_hash.freeze @columns_hash.each do |name, column| type = connection.lookup_cast_type_from_column(column) type = _convert_type_from_options(type) warn_if_deprecated_type(column) define_attribute( name, type, default: column.default, user_provided_default: false ) end end
default: column.default
これは怪しいですね。これはcolumnがどんなクラスで、どんな値を持っているか是非知りたいところです。 ところがcolumnを取得する前にconnectionを取るところで例外が起きます。
というのは、DBの設定を一切せずにActiveRecordのサブクラスを扱っているからです。 ここから先は、DBを用意した方が良さそうです。
といったところで、今日は時間切れです。