1
/
5

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/railsdelegated_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

https://github.com/rails/rails/blob/86de9c223ad1b180dc80d69df22bd64981e4d793/activerecord/lib/active_record/delegated_type.rb#L170-L173

delegated_type が追加するメソッドのうち、 #{singular}_id の実装では primary key や foreign key の名前がハードコードされているため、uuid などを primary key にすると動きません。今回 delegated_type を使った output も primary key の名前が id ではなく uuid であったため、このケースに該当していました。

role_id   = "#{role}_id"

https://github.com/rails/rails/blob/86de9c223ad1b180dc80d69df22bd64981e4d793/activerecord/lib/active_record/delegated_type.rb#L178

define_method "#{singular}_id" do
  public_send(role_id) if public_send(query)
end

https://github.com/rails/rails/blob/86de9c223ad1b180dc80d69df22bd64981e4d793/activerecord/lib/active_record/delegated_type.rb#L203-L205

以下が修正後の 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 自体を修正することで解決しました。小さい修正でしたが、誰かの役に立つことができれば幸いです。

Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?
Wantedly, Inc.では一緒に働く仲間を募集しています
13 いいね!
13 いいね!

今週のランキング

江草 亮太さんにいいねを伝えよう
江草 亮太さんや会社があなたに興味を持つかも