ポリモーフィック関連/Active Storage/ActiveStorage::Blob/ActiveStorage::Attachment/has_one_attached

ポリモーフィック関連

ポリモーフィック関連とは、ある1つのカラムが複数のテーブルを参照しているようなパターンの関連を表したもので、

例)Q&Aアプリなどのcommentsテーブルをイメージするとよい

同じようなカラムを持っているテーブルが複数ある場合、

例)teacher_id, student_id 等をイメージ

それらを1つのテーブルで管理してしまうことができる。複数のクラスのインターフェースを統一し、同じように扱えるようにする。 一つのモデルを同じインターフェースを持ったものが扱う関連の仕方。 ダックタイピングというコード概念からの考えで、インタフェースを統一することで冗長なコードがなくなり、スケーラビリティを担保できる。

(例)ポリモーフィック関連を使わなかった場合

class Commment < ApplicationRecord
  belongs_to :teacher
  belongs_to :student
  # メッセージを送るモデルが増えたら、それに従いbelongs_toの数も増えていく
end

class Teacher < ApplicationRecord
  has_many :comments
end

class Student < ApplicationRecord
  has_many :comments
end
(実行例)
teacher = Teacher.find(params[:teacher_id])
teacher.comments 
=> #select * from comments where teacher_id = ?

comment = Comment.find(params[:comment_id])
comment.teacher
=> #select * from teachers where teacher_id = ?

(例)ポリモーフィック関連を使用した場合

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
  # メッセージを送るモデルが増えてもbelongs_toの数は増えない
end

class Teacher < ApplicationRecord
  has_many :comments, as: :commentable
end

class Student < ApplicationRecord
  has_many :comments, as: :commentable
end
(実行例)
# commentableを通すことで、TeacherとStudentのどちらであるかを意識する必要がない。
Comment.find(params[:id]).commentable

# コメント送信者モデル側からは通常のhas_many関連として扱える
Teacher.find(params[:id]).comments
Student.find(params[:id]).comments

commentableなモデルを追加する時にも上記のように追加モデルを書くだけでよく、既存コードをいじる必要はありません。

ダックタイピングとかポリモーフィック関連ってなんのこっちゃわからなかったけど久しぶり読んでみたら、すごくコードがすっきりできた。

参照:Railsのポリモーフィック関連とはなんなのか

Active Storageで画像を添付する

Taskモデルに画像を添付したかったので、Active Storageを使用することにした。

$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate

コマンドを実行しインストールすると、

Active Storageで使用する

  • active_storage_blobs テーブル
  • active_storage_attachments テーブル

二つのテーブルを生成するマイグレーションファイルができます。

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end
  end
end

ActiveStorage::Blob

ActiveStorage::Blobモデルは添付されたファイルに対応していて、active_storage_blobsテーブルに添付画像のデータが管理されていると考えてください。 またファイルの実体をデータベース外で管理することを前提にしていて、識別keyファイル名コンテンツタイプメタデータサイズなどを管理している。

ActiveStorage::Attachment

ActiveStorage::Attachmentモデルは 「①アプリ内の画像添付するモデル(ここでいうと、Taskモデル)」と「②ActiveStorage::Blobモデル」との中間テーブルの役割を果たしているテーブルのモデルです。

①とはFKカラム名をFK値として保持しており、ポリモーフィック関連となっていて、一方②は直接的なidのみで紐付けされている。

勝手なイメージ

それぞれのモデルとActiveStorage::Attachmentモデルで外部キーの関連で画像の関連付けをして、画像のデータなどの細かい情報に関してはActiveStorage::Blobモデルを別に作成して、そっちで管理するようにしているんだろうなぁ。と思っています。

本来、ActiveStorage::AttachmentモデルActiveStorage::Blobモデルを別々にしなくてもいいけど、一緒にするとテーブル構造がカラム が多くなって見づらいから、別々にしたよ。みたいな感じで考えて落ち着きました。

Taskモデルに画像添付できるようにする

# Taskモデルにファイルを添付するためにimageを関連付けする
class Task < ApplicationRecord
  has_one_attached :image
end

# 添付の有無を確認する
task.image.attached? # => true