nyamadori / rails_cti_pof

Rails でのクラステーブル継承の検証実装
0 stars 0 forks source link

クラステーブル継承の API を Rails でどう実装するか(あるいはどうモンキーパッチするか)調査 #1

Open nyamadori opened 5 years ago

nyamadori commented 5 years ago

何がしたいか

https://github.com/nyamadori/rails_cti_pof/blob/master/spec/models/feature_request_spec.rb のような API を実装したい。

Rails version

5.2.3

実装方法の調査

https://github.com/nyamadori/rails_cti_pof/commit/21d6f89c02c93f554213eca501f09762689a3950 のコミット上で実現したい仕様を書いた rspec を実行

$ rspec --backtrace
      feature_request = FeatureRequest.create(
         **issue_attributes,
         **feature_request_attributes,
       )

     ActiveModel::UnknownAttributeError:
       unknown attribute 'sponser' for FeatureRequest.
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_assignment.rb:53:in `_assign_attribute'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_assignment.rb:44:in `block in _assign_attributes'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_assignment.rb:43:in `each'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_assignment.rb:43:in `_assign_attributes'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/attribute_assignment.rb:23:in `_assign_attributes'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activemodel-5.2.3/lib/active_model/attribute_assignment.rb:35:in `assign_attributes'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/core.rb:315:in `initialize'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/inheritance.rb:67:in `new'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/inheritance.rb:67:in `new'
     # /Users/manabu.nakajima/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.3/lib/active_record/persistence.rb:35:in `create'
     # ./spec/models/feature_request_spec.rb:68:in `block (3 levels) in <top (required)>'

レコード作成に失敗しているので、まずはそこから調べる。

次の場所に Binding.pry を仕掛け、rspec を実行

activerecord-5.2.3/lib/active_record/persistence.rb:35:in `create'

pry で inspect

[1] pry(FeatureRequest)> self
=> FeatureRequest(id: integer, user_id: integer, product_id: integer, status: integer, title: string, description: text, priority: integer, version_resolved: string, created_at: datetime, updated_at: datetime)

CTI を実装する上で、FeatureRequest クラスにもかかわらず、スキーマ定義が issues テーブルのものになる仕様は困るが、Rails で STI を実装するにはこれが最も自然だと思われる。STI と CTI を一アプリケーション内で同時に使えるようにしたいので、この仕様は互換性を保った状態にする必要がある。

また、このエラーを防ぐには ActiveRecord が認識するスキーマ定義を変えて、スキーマ定義に sponser が入るようにすれば良さそうだが、悪手だと思う。というのは、実際のテーブルの状態と異なるスキーマ定義ができてしまうからだ。

     ActiveModel::UnknownAttributeError:
       unknown attribute 'sponser' for FeatureRequest.

STI との互換性を保つため、何らかの方法でモデルが STI のモデルか、CTI のモデルかを区別できるようにする。

STI の場合は、ベースクラスのスキーマ定義、CTI の場合は、自身のモデルのスキーマ定義を見るようにする。

nyamadori commented 5 years ago

STI の場合は、ベースクラスのスキーマ定義、CTI の場合は、自身のモデルのスキーマ定義を見るようにする

した。

rspec を実行すると、今度はこのエラーが出たので調べる。

     Failure/Error:
       FeatureRequest.create(
         **issue_attributes,
         **feature_request_attributes,
       )

     ActiveModel::MissingAttributeError:
       can't write unknown attribute `user_id`
     # ./spec/models/feature_request_spec.rb:35:in `block (3 levels) in <top (required)>'

can't write unknown attribute user_id

と言われるが、 FeatureRequest.reflections にはベースクラスである Issue から継承した user がちゃんと存在する。

[2] pry(main)> FeatureRequest.reflections
=> {"user"=>
  #<ActiveRecord::Reflection::BelongsToReflection:0x00007fb1b0202da8
   @active_record=
    Issue(id: integer, user_id: integer, product_id: integer, status: integer, title: string, description: text, prior
   @association_scope_cache=#<Concurrent::Map:0x00007fb1b0202a88 entries=0 default_proc=nil>,
   @constructable=true,
   @foreign_type=nil,

activemodel-5.2.3/lib/active_model/attribute_set.rb:58:in `write_from_user' に

p self[name]

を仕掛けて name = user の属性情報を得る。

これ。
#<ActiveModel::Attribute::Null:0x00007fd5ceaefc58 @name="user_id", @value_before_type_cast=nil, @type=#<ActiveModel::Type::Value:0x00007fd5ce50c2a0 @precision=nil, @scale=nil, @limit=nil>, @original_attribute=nil>

すると上記が得られる。ActiveModel::Attribute::Null は、指定された属性名に対応する属性情報がない場合に返ってくる型らしい(いわゆる Null Object パターン)。

属性情報がある場合は以下が得られる。

#<ActiveModel::Attribute::FromDatabase:0x00007fd5ce507980 @name="id", @value_before_type_cast=nil, @type=#<ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer:0x00007fd5d31cec50 @precision=nil, @scale=nil, @limit=nil, @range=-9223372036854775808...9223372036854775808>, @original_attribute=nil, @value=nil>

ちなみに :user という関連名から外部キーカラム名の解決は以下で行われる。

activerecord-5.2.3/lib/active_record/associations/belongs_to_association.rb:98:in

nyamadori commented 5 years ago

association の情報はスーパークラスから引き継がれるが、属性の情報は self.table_name = で指定したテーブル名から作成されるので、can't write unknown attributeuser_id` というエラーが出るようだ。

nyamadori commented 5 years ago

ActiveModel::AttributeSet#[] をオーバライドして、モデルのスーパークラスの定義を見に行くようにすればいいかと思ったけど、ActiveModel::AttributeSet のコンテキストでは、モデルのスーパークラスを見に行けないことが分かった。

nyamadori commented 5 years ago

だから、テーブルスキーマを読み込み属性情報を作る段階で、モデルのスーパークラスの定義を参照するようにする必要がありそうだ。