今回はRails6.1からActive::Recordに追加されるdelegated_typeについて解説したいと思います。2021/5/1現在ではdelegated_typeについての参考文献や実装サンプルは多くはありませんが、今後大規模開発を中心に導入されそうな印象を抱いた機能です。
delegated_typeの概要
delegated_typeとは、あるモデルから他のモデルの属性を引き継ぐ際に用いられる方法の1つです。その他の方法として代表的なものは、STI(Single Table Inheritance、単一テーブル継承)が挙げられます。
STIとの違いは以下です。
- STI: 親クラスと子クラスの属性をまとめて1つのテーブルに保存
- delegated_type: 共通で使用するものは委譲元のテーブルに保存し、個別で使用するものについては委譲先のテーブルに分けて保存
STIとは
STIとは「Single Table Inheritance」の略で単一テーブル継承と訳されます。
例えば、ユーザーが持っている漫画をジャンル毎に管理するアプリケーションを作ろうとした場合、以下のようなテーブル設計にすることができます。(シンプルにするためカラムはuser_idのみにしています)

このようなテーブル設計でも実装できなくはないのですが、新しくジャンルを追加したい場合は新たにテーブルを増やさなければならないというデメリットがあります。「ジャンルの数=テーブル数」となり、将来的にメンテナンスが大変なアプリケーションになってしまいそうです。
また、boys/youth/girlsに共通したデータでもそれぞれのテーブルに記述しなければならず、コードの肥大化に繋がってしまうこともデメリットとしてあげられます。
ここでSTIが登場。
下図のようにboys/youth/girlsをmangaというテーブルに全てまとめ1つのテーブルで管理することで、ジャンルが増えても新しくテーブルを追加せずに済みます。

user_idの他にtypeというカラムがmangaテーブルにしれっと追加されていますが、ここには子テーブルであるboys/youth/girlsのデータが入ることになります。
id | type | user_id |
1 | boys | 1 |
2 | girls | 1 |
3 | youth | 2 |
ではgirlsテーブルのみに存在するようなカラムはどうすれば良いのでしょうか。
例としてgirlsテーブルのみにpriceカラムを追加したとします(そのようなテーブル設計にはならないとは思いますが)。

その場合はmangaテーブルにpriceカラムを追加しなければなりません。
このように子テーブル固有のカラムも全て親テーブルに追加しなければならないという点に注意しましょう。

では保存(create)した場合はどうなるのでしょうか。
priceカラムを持たないboysやyouthをcreateした場合、priceにはnullが入ることになります。そのため、priceのような子テーブル固有のカラムにNOT NULL制約をつけることはできません。
id | type | price | user_id |
1 | boys | null | 1 |
2 | girls | 1000 | 1 |
3 | youth | null | 2 |
続いて実装の注意点です。
実装の注意点としては、子が継承するのはActiveRecord::Baseでなく親であるMangaを継承しなければならないということです。
class Manga < ActiveRecord::Base
belongs_to :user
end
class Boys < Manga
end
class Youth < Manga
end
class Girls < Manga
end
STIの実装ポイント
- 親にtypeカラムを持たせる
- 子に特有なカラムが必要であれば、それも親に持たせる
- 子の継承元はActiveRecord::Baseではなく親クラス
STIのメリット
- テーブルが少なくて済む
- 論理的なレコード全体を取得するのにジョインが不要
- レコードのサブクラスの変更が容易
STIのデメリット
- 特定のサブクラスに固有の属性に対してNOT NULL制約を適用できない
- 頻繁に使用しないカラムの列ではNULLばかりになってしまう
- カラム数が多くなりやすい
- サードパーティgemとの相性(gemがSTIでの利用を考慮していない)
So STI works best when there’s little divergence between the subclasses and their attributes.(つまりSTIが最適なのは、サブクラス同士やその属性同士の違いが極力少ない場合ということになる。)
参照: https://github.com/rails/rails/pull/39341
delegated_typeとは
delegated_typeは、共通で使用するものは移譲元(親テーブル)に保存し、個別で使用するものはそれぞれの移譲先(子テーブル)に分けて保存する機能です。これにより、STIのデメリットをうまく補完してくれると言えます。
先程の漫画を管理するアプリケーションをdelegated_typeを用いると以下のようになります。Mangable concernを作成して委譲先にincludeしている点と委譲先の継承元がApplicationRecordである点が大きなポイントになります。
class Manga < ApplicationRecord
belongs_to :user
delegated_type :mangable, types: %w(Boys Youth Girls)
end
class Boys < ApplicationRecord
include Mangable
end
class Youth < ApplicationRecord
include Mangable
end
class Girls < ApplicationRecord
include Mangable
end
module Mangable
extend ActiveSupport::Concern
included do
has_one :manga, as: :mangable, touch: true
end
end
まとめ
- STIは親クラスと子クラスの属性をまとめて1つのテーブルに保存する
- delegated_typeは共通で使用するものは委譲元のテーブル、個別で使用するものについては委譲先に保存する
- STIのデメリットであるNOT NULL制約がかけられない、カラムが増えてしまうといったデメリットをdelegated_typeを用いることで解消できる
参考
- Add delegated type to Active Record
- みんなRailsのSTIを誤解してないか!?
- 週刊Railsウォッチ(20200601前編)Active Recordに新機能「delegated typing」追加、RuboCopのデフォルト設定アンケートほか
今回はRails6.1からActive::Recordに追加されるdelegated_typeについて解説しました。今後様々な情報やサンプルが公開されてくると思うので、随時この記事も更新していきます。