概要
CTIで設計したテーブルをアプリケーションに落とし込むためにDelegated typesを利用したのでその振り返り
背景
- サブクラスごとに異なるカラムを使う前提があった
- 今後の拡張時に既存処理の影響を与えずにロジックを記述できるようにしたかった
- そのため、STIよりかはCTI(MTI)的にサブクラスごとにテーブルを分けてその状態を実現したかった
- STIのような共通インターフェースでサブクラスにアクセスしたかった
要は、サブクラスごとにテーブルを分けたいけれど、STIのように共通インターフェースでサブクラスにアクセスしたかった。
最初はhttps://max.engineer/mti#multiple-table-inheritance-simulatedなどを見て、メタプロで実現しようと考えていた。
ただ、納期が迫っていたのでメタプロでやり切れるのか不明瞭だったのと、今後の保守性 / 拡張性まで考えると懸念があった。
調査した結果、https://stackoverflow.com/a/63273663 でどうやらMTIがRails6.1.0からネイティブサポートされたという情報を見つけた。それがDelegated types。
メタプロよりネイティブサポートされた機能を使う方がベターだと考え、これを使って実装をしていこうと決断。(そのためにRailsのバージョンまで上げた)
詳細
Delegated typesは委譲(Delegate)を使って、スーパークラスから具象サブクラスへアクセスできるようにしている。
クラス設計は下記のようになる。
まず、スーパークラスとしてEntryが存在しており、具象サブクラスとしてMessage
/ Comment
が存在している。
スーパークラスにはdelegated_type
の宣言をしており、サブクラスにはEntryable
モジュールをincludeしている。
1# Schema: entries[ id, account_id, creator_id, created_at, updated_at, entryable_type, entryable_id ] 2class Entry < ApplicationRecord 3 belongs_to :account 4 belongs_to :creator 5 delegated_type :entryable, types: %w[ Message Comment ] 6end 7 8module Entryable 9 extend ActiveSupport::Concern 10 included do 11 has_one :entry, as: :entryable, touch: true 12 end 13end
1# Schema: messages[ id, subject, body ] 2class Message < ApplicationRecord 3 include Entryable 4end 5 6# Schema: comments[ id, content ] 7class Comment < ApplicationRecord 8 include Entryable 9end
スーパークラスのentryable_id
に具象サブクラスのプライマリID、entryable_type
にサブクラスのtypeが格納される。
スーパークラス側のdelegated_type
メソッドのtypes
オプションで許可するtypeを定義できる。
上記例では、Message
とComment
typeのみ許可することになっている。
この時、スーパークラスとサブクラスのレコードは下記のようになっている。
1[1] pry(main)> Entry.first 2=> #<Entry:0x0000ffff7c3996b8 3 id: 1, 4 account_id: 1, 5 creator_id: 1, 6 entryable_id: 1, 7 entryable_type: "Comment" 8 created_at: Wed, 26 Oct 2022 07:04:34 UTC +00:00, 9 updated_at: Mon, 28 Nov 2022 05:42:32 UTC +00:00, 10 > 11 [1] pry(main)> Comment.first 12=> #<Comment:0x0000ffff7c3996b8 13 id: 1, 14 content: "content" 15 >
これにより、下記のようにスーパークラスからentryable
インターフェースを経由して具象サブクラスにアクセスできる。(ifによるtype分岐などは必要としない。尋ねるな命じよ(Tell, Don't Ask!))
1Entry.first.entryable 2# => Comment.first 3 4Entry.first.entryable.id 5# => Comment.first.id 6 7Entry.first.entryable.content 8# => Comment.first.content
まとめ
Delegated types を選定することで新たな知見を得られたのが良かった。
STIとCTIの違いを抑えられただけでなく、Railsのアップグレード(こちらの記事)についても知見を得られたのもGood。