- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (19)
- Development
- Business
Rails 6.1 の delegated_type にプルリクエストを送った話
Photo by Andrew Ridley on Unsplash
Wantedly でエンジニアをしている江草です。
Rails 6.1 でクラスの階層構造と RDB のテーブルを対応付ける新しい方法として、delegated_type が追加されました。
https://github.com/rails/rails/pull/39341
上記の delegated_type を追加した
プルリクエストにも書かれていますが、delegated_type
を使った方法では、独自のテーブルを持ったスーパークラスがそのサブクラスで共通の属性を保存し、サブクラスもそれぞれ独自のテーブルと属性を持ちます。そして、階層を成して責務を共有するために、委譲(delegation) を用います。
僕が社内で delegated_type を使ったときに問題にあたり
、https://github.com/rails/rails の delegated_type
を修正するプルリクエストを送りマージされたため、この記事ではそれまでの経緯とプルリクエストの内容について書きます。
なぜ `delegated_type` を使うのか
Wantedly の新しいプロフィールでは、プロフィールに対して画像、数値など様々な種類の output を追加することができます。
これらの output は以下のようにそれぞれ異なるテーブルに保存されています。
# Schema: visual_outputs[ uuid, experience_uuid, ... ]
class VisualOutput < ActiveRecord::Base
end
# Schema: number_outputs[ uuid, experience_uuid, ... ]
class NumberOutput < ActiveRecord::Base
end
...
output に関係するロジックを実装するときに、それぞれの output 固有の属性は必要ないが、複数種類の output にまたがるクエリを書きたくなることがあります。そこで全種類の output のテーブルに対してそれぞれクエリを書いていたのですが、 output の種類の数だけクエリが発行されてしまいパフォーマンスに問題が出ていました。
そこで以下のように全 output に共通の属性を持つテーブルを追加した上で delegated_type を導入し
、複数種類の output にまたがるクエリを書く時は1つのテーブルに対してだけクエリを発行するだけで済むようにしました。
# Schema: outputs[ uuid, experience_uuid, ... ]
class Output < ActiveRecord::Base
end
送ったプルリクエスト
delegated_type
の内部実装では、 belongs_to
を呼んだ上で define_delegated_type_methods で
delegated_type
固有のメソッドを追加しています。ここで belongs_to
の引数には delegated_type
への option に対して polymorphic: true
を追加したものがそのまま渡されています。
def delegated_type(role, types:, **options)
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
define_delegated_type_methods role, types: types
end
delegated_type が追加するメソッドのうち、 #{singular}_id
の実装では primary key や foreign key の名前がハードコードされているため、uuid
などを primary key にすると動きません。今回 delegated_type
を使った output も primary key の名前が id
ではなく uuid
であったため、このケースに該当していました。
role_id = "#{role}_id"
define_method "#{singular}_id" do
public_send(role_id) if public_send(query)
end
以下が修正後の define_delegated_type_methods です。
def define_delegated_type_methods(role, types:, options:)
primary_key = options[:primary_key] || "id"
role_type = "#{role}_type"
role_id = options[:foreign_key] || "#{role}_id"
define_method "#{role}_class" do
public_send("#{role}_type").constantize
end
define_method "#{role}_name" do
public_send("#{role}_class").model_name.singular.inquiry
end
types.each do |type|
scope_name = type.tableize.gsub("/", "_")
singular = scope_name.singularize
query = "#{singular}?"
scope scope_name, -> { where(role_type => type) }
define_method query do
public_send(role_type) == type
end
define_method singular do
public_send(role) if public_send(query)
end
define_method "#{singular}_#{primary_key}" do
public_send(role_id) if public_send(query)
end
end
end
https://github.com/rails/rails/pull/40865
#{singular}_id
メソッドでは primary_key
を考慮するようにした上で、その実装でも foreign_key
を考慮するようにしています。delegated_type が
belongs_to
をラップする実装になっているため、新しい option を追加することなく問題を修正することができました。
送ったプルリクエストはドキュメントについて指摘を受けただけですぐにマージされました。
まとめ
今回は delegated_type
でデフォルト以外の primary key と foreign key を使った時の問題を、delegated_type
自体を修正することで解決しました。小さい修正でしたが、誰かの役に立つことができれば幸いです。