Delegated typesを利用した際の知見

概要

CTIで設計したテーブルをアプリケーションに落とし込むためにDelegated typesを利用したのでその振り返り

背景

要は、サブクラスごとにテーブルを分けたいけれど、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している。

# Schema: entries[ id, account_id, creator_id, created_at, updated_at, entryable_type, entryable_id ]
class Entry < ApplicationRecord
  belongs_to :account
  belongs_to :creator
  delegated_type :entryable, types: %w[ Message Comment ]
end

module Entryable
  extend ActiveSupport::Concern
  included do
  has_one :entry, as: :entryable, touch: true
  end
end
# Schema: messages[ id, subject, body ]
class Message < ApplicationRecord
  include Entryable
end

# Schema: comments[ id, content ]
class Comment < ApplicationRecord
  include Entryable
end

スーパークラスのentryable_idに具象サブクラスのプライマリID、entryable_typeにサブクラスのtypeが格納される。

スーパークラス側のdelegated_typeメソッドのtypesオプションで許可するtypeを定義できる。

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

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

[1] pry(main)> Entry.first
=> #<Entry:0x0000ffff7c3996b8
    id: 1,
    account_id: 1,
    creator_id: 1,
    entryable_id: 1,
    entryable_type: "Comment"
    created_at: Wed, 26 Oct 2022 07:04:34 UTC +00:00,
    updated_at: Mon, 28 Nov 2022 05:42:32 UTC +00:00,
    >
    [1] pry(main)> Comment.first
=> #<Comment:0x0000ffff7c3996b8
    id: 1,
    content: "content"
    >

これにより、下記のようにスーパークラスからentryableインターフェースを経由して具象サブクラスにアクセスできる。(ifによるtype分岐などは必要としない。尋ねるな命じよ(Tell, Don't Ask!))

Entry.first.entryable
# => Comment.first

Entry.first.entryable.id
# => Comment.first.id

Entry.first.entryable.content
# => Comment.first.content

まとめ

Delegated types を選定することで新たな知見を得られたのが良かった。

STIとCTIの違いを抑えられただけでなく、Railsのアップグレード(こちらの記事)についても知見を得られたのもGood。