0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsのポリモーフィック関連 - 1つのテーブルで複数モデルに紐づける設計

0
Posted at

背景

  • 添付ファイル機能を複数のモデル(チケット、メッセージ、コメントなど)に持たせたいケースがあった
  • 愚直にやるとモデルごとに外部キーカラムを追加する必要があり、テーブル設計が肥大化する
  • Railsのポリモーフィック関連(Polymorphic Association)を使えば、1つのテーブルで複数モデルへの紐づけを管理できると知ったのでまとめる

ポリモーフィック関連とは

1つのモデル(テーブル)が、複数の異なるモデルに対して belongs_to できる仕組み

通常の belongs_to は1つの親モデルに固定されるが、ポリモーフィックでは _type_id の2カラムで「どのモデルの、どのレコードか」を表現する

通常の関連との違い

通常の関連(ポリモーフィック不使用の場合):

  • モデルごとに別々の外部キーカラムが必要(例: ticket_id, message_id, comment_id
  • 紐づくモデルが増えるたびにカラム追加のマイグレーションが必要
  • 大半のカラムが NULL になり非効率

ポリモーフィック関連:

  • attachable_type(モデル名)と attachable_id(レコードID)の2カラムだけで管理
  • 新しいモデルに添付機能を追加するときもマイグレーション不要

データの管理方法

attachmentsテーブルのデータイメージ:

id attachable_type attachable_id 意味
1 "Ticket" 10 Ticket#10の添付
2 "Message" 25 Message#25の添付
3 "Ticket" 10 Ticket#10の2個目の添付
4 "Comment" 42 Comment#42の添付

attachable_type にモデル名、attachable_id にそのモデルのレコードIDが入る。この2つの組み合わせで「どのモデルの、どのレコードに紐づくか」を一意に特定する

マイグレーション

class CreateAttachments < ActiveRecord::Migration[8.0]
  def change
    create_table :attachments do |t|
      t.string :name
      t.belongs_to :attachable, polymorphic: true
      # ↑ これだけで attachable_type (string) と attachable_id (bigint) の
      #   2カラム + インデックスが自動生成される
      t.timestamps
    end
  end
end

t.belongs_topolymorphic: true を渡すと、Railsが _type_id の2カラムを自動的に作成する。手動で t.string :attachable_typet.bigint :attachable_id を書く必要はない

Railsでの書き方

基本的なモデル定義

# belongs_to側(子モデル) — polymorphic: true を付ける
class Attachment < ApplicationRecord
  belongs_to :attachable, polymorphic: true
end

# has_many側(親モデル) — as: :attachable で対応付け
class Ticket < ApplicationRecord
  has_many :attachments, as: :attachable, dependent: :destroy
end

class Message < ApplicationRecord
  has_many :attachments, as: :attachable, dependent: :destroy
end
  • belongs_to :attachable, polymorphic: trueattachable_typeattachable_id の2カラムを使って、任意のモデルに紐づけることを宣言する
  • has_many :attachments, as: :attachable — 「自分が attachable として紐づけられている Attachment を持つ」ことを宣言する。as: オプションがポリモーフィック関連のキーワード
  • dependent: :destroy — 親レコードが削除されたとき、紐づく Attachment も一緒に削除する

concernで共通化する

添付機能を使うモデルが複数ある場合、concernとして切り出すのが一般的

# app/models/concerns/attachable.rb
module Attachable
  extend ActiveSupport::Concern

  included do
    has_many :attachments, as: :attachable, dependent: :destroy
  end
end

# 使う側は include するだけ
class Ticket < ApplicationRecord
  include Attachable
end

class Message < ApplicationRecord
  include Attachable
end

class Comment < ApplicationRecord
  include Attachable
end

ActiveSupport::Concern はRailsが提供するモジュール拡張の仕組みで、included ブロック内のコードは include された時点で実行される。これにより、新しいモデルに添付機能を追加するときは include Attachable の一行だけで済む

Active Storageと組み合わせた設計例

ポリモーフィック関連で添付管理モデルを作り、ファイル本体はActive Storageに任せるパターン

Ticket/Message/Comment
  → (polymorphic has_many) → Attachment
    → (Active Storage has_one_attached) → data_file (blob)
class Attachment < ApplicationRecord
  belongs_to :attachable, polymorphic: true
  has_one_attached :data_file  # Active Storageでファイル本体を管理
end

has_one_attached はActive Storageのメソッドで、ファイルのアップロード・ダウンロード・削除をRailsの規約に沿って扱える。ポリモーフィック関連が「どのモデルに紐づくか」を管理し、Active Storageが「ファイル本体の保存先」を管理する、という役割分担になる

メリット・デメリット

内容
メリット テーブル1つで済むためスキーマがシンプル。新しいモデルへの機能追加がマイグレーション不要(include するだけ)。concernと組み合わせることでコードをDRYに保てる
デメリット DBレベルの外部キー制約が張れない(attachable_type が文字列のため、RDBMSは参照先テーブルを特定できない)。_type + _id の複合条件でJOINするためクエリが若干複雑になる

外部キー制約が張れない問題について

通常の belongs_to では add_foreign_key :attachments, :tickets のようにDB側で参照整合性を保証できるが、ポリモーフィック関連では attachable_type"Ticket" なのか "Message" なのかはアプリケーション層でしか判断できない。そのため、参照整合性はRailsの dependent: :destroy やバリデーションで担保する必要がある

ポリモーフィック関連が適しているケース

  • 添付ファイル — 複数モデルにファイル添付機能を持たせたいとき
  • コメント — チケット、記事、タスクなど複数の対象にコメント機能を付けたいとき
  • タグ付け — 異なる種類のリソースに共通のタグ機能を提供したいとき
  • いいね(Like) — 投稿、コメント、写真など複数の対象にリアクション機能を付けたいとき

共通点は「同じ構造の機能を、複数の異なるモデルで使いたい」というパターン

まとめ

  • ポリモーフィック関連は「1つのテーブルで複数モデルへの紐づけを管理する仕組み」
  • _type + _id の2カラムで親モデルを特定する
  • t.belongs_to :attachable, polymorphic: true でマイグレーションを書き、belongs_to :attachable, polymorphic: truehas_many :attachments, as: :attachable でモデルを定義する
  • concernと組み合わせることで include Attachable の一行で機能追加できる
  • DBレベルの外部キー制約が使えないトレードオフがあるため、参照整合性はアプリケーション層で担保する

感想

  • concernとポリモーフィックが綺麗にハマるのが美しいなと思いました

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?