Rails

【Rails】Rails6.1から追加されるdelegated_typeについて簡単にまとめてみた【STIについても解説】

Rails

 

今回はRails6.1からActive::Recordに追加されるdelegated_typeについて解説したいと思います。2021/5/1現在ではdelegated_typeについての参考文献や実装サンプルは多くはありませんが、今後大規模開発を中心に導入されそうな印象を抱いた機能です。

delegateを直訳すると「委任する・委託する」といった意味になります。

delegated_typeの概要

delegated_typeとは、あるモデルから他のモデルの属性を引き継ぐ際に用いられる方法の1つです。その他の方法として代表的なものは、STI(Single Table Inheritance、単一テーブル継承)が挙げられます。

STIとの違いは以下です。

  • STI: 親クラスと子クラスの属性をまとめて1つのテーブルに保存
  • delegated_type: 共通で使用するものは委譲元のテーブルに保存し、個別で使用するものについては委譲先のテーブルに分けて保存

STIとは

STIとは「Single Table Inheritance」の略で単一テーブル継承と訳されます。

例えば、ユーザーが持っている漫画をジャンル毎に管理するアプリケーションを作ろうとした場合、以下のようなテーブル設計にすることができます。(シンプルにするためカラムはuser_idのみにしています)

テーブル構成
boys→少年漫画、youth→青年漫画、girls→少女漫画となります。

 

このようなテーブル設計でも実装できなくはないのですが、新しくジャンルを追加したい場合は新たにテーブルを増やさなければならないというデメリットがあります。「ジャンルの数=テーブル数」となり、将来的にメンテナンスが大変なアプリケーションになってしまいそうです。

また、boys/youth/girlsに共通したデータでもそれぞれのテーブルに記述しなければならず、コードの肥大化に繋がってしまうこともデメリットとしてあげられます。

ここでSTIが登場。

下図のようにboys/youth/girlsをmangaというテーブルに全てまとめ1つのテーブルで管理することで、ジャンルが増えても新しくテーブルを追加せずに済みます。

STIを用いたテーブル構成
オレンジで囲まれた部分のテーブルは不要になります。モデルは必要です。

 

user_idの他にtypeというカラムがmangaテーブルにしれっと追加されていますが、ここには子テーブルであるboys/youth/girlsのデータが入ることになります。

idtypeuser_id
1boys1
2girls1
3youth2

 

ではgirlsテーブルのみに存在するようなカラムはどうすれば良いのでしょうか。

例としてgirlsテーブルのみにpriceカラムを追加したとします(そのようなテーブル設計にはならないとは思いますが)。

priceカラムの追加

 

その場合はmangaテーブルにpriceカラムを追加しなければなりません。

このように子テーブル固有のカラムも全て親テーブルに追加しなければならないという点に注意しましょう。

priceカラムを追加した場合のテーブル構成

 

では保存(create)した場合はどうなるのでしょうか。

priceカラムを持たないboysやyouthをcreateした場合、priceにはnullが入ることになります。そのため、priceのような子テーブル固有のカラムにNOT NULL制約をつけることはできません。

idtypepriceuser_id
1boysnull1
2girls10001
3youthnull2
ここで想像が付くかもしれませんが、頻繁に使用しないカラムが存在してしまうと、nullだらけのテーブルになってしまうというデメリットがあります。

 

続いて実装の注意点です。

実装の注意点としては、子が継承するのは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

DHHがこう主張するのも納得ですね。

delegated_typeとは

delegated_typeは、共通で使用するものは移譲元(親テーブル)に保存し、個別で使用するものはそれぞれの移譲先(子テーブル)に分けて保存する機能です。これにより、STIのデメリットをうまく補完してくれると言えます。

delegated_typeは継承を行っていないため、親子関係ではありません。そのため移譲元と移譲先という表現を用いています。

 

先程の漫画を管理するアプリケーションを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を用いることで解消できる

参考

 

 

今回はRails6.1からActive::Recordに追加されるdelegated_typeについて解説しました。今後様々な情報やサンプルが公開されてくると思うので、随時この記事も更新していきます。