Delegated typesを利用した際の知見

published_at: 2022-12-25

概要

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を定義できる。

上記例では、MessageCommenttypeのみ許可することになっている。

この時、スーパークラスとサブクラスのレコードは下記のようになっている。

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。