単一テーブル継承

単一テーブル継承

あるクラスから派生する全てのサブクラスを交通の1つのテーブルに対応させます。DBにある1つのテーブルを、複数のモデルで共有利用することができます。


テーブル設計の概要

記事テーブルにタグ、カテゴリー、著者テーブルを関連付する。

Image from Gyazo

このTaxnomiesテーブルを使ってSTIという継承関係を実現する。


db/schema.rb

  create_table "taxonomies", force: :cascade do |t|
    t.string "type"
    t.string "name"
    t.string "slug"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["slug"], name: "index_taxonomies_on_slug"
    t.index ["type"], name: "index_taxonomies_on_type"
  end

schema.rbでtaxonomiesモデルが作成されています。しかし、tag、category、authorのモデルテーブルはありません。

それもそのはず、単一テーブル継承においてTaxonomieはスーパークラスで、DB上に実在するテーブルです。

Category、Tag、AuthorクラスはTaxonomyクラスを継承したサブクラスだが、categories、tags、authorsテーブルというものはDB上に存在しない。しかし、STIを使用することで、開発者はCategories、Tags、Authorsテーブルというテーブルが存在するかのようにモデルとDBを扱えるようになるのです。


taxonomy.rb

class Taxonomy < ApplicationRecord
  validates :name, presence: true, uniqueness: { scope: :type }, length: { maximum: 16 }
  validates :slug, presence: true, uniqueness: { scope: :type }, length: { maximum: 64 }, slug_format: true
end

category.rb

class Category < Taxonomy
  has_many :articles
end

tag.rb

class Tag < Taxonomy
  has_many :article_tags
  has_many :articles, through: :article_tags
end

author.rb

class Author < Taxonomy
  has_many :articles

  has_one_attached :avatar

  validates :avatar, attachment: { purge: true, content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 10_485_760 }
end


クエリは以下のようになります。

[61] pry(main)> Author.all
  Author Load (1.1ms)  SELECT "taxonomies".* FROM "taxonomies" WHERE "taxonomies"."type" IN ('Author')
[62] pry(main)> Category.all
  Category Load (0.3ms)  SELECT "taxonomies".* FROM "taxonomies" WHERE "taxonomies"."type" IN ('Category')
[63] pry(main)> Tag.all
  Tag Load (0.3ms)  SELECT "taxonomies".* FROM "taxonomies" WHERE "taxonomies"."type" IN ('Tag')



注意点

継承元のテーブルにはtypeカラムを実装しておく必要がある。 どういうことかと言うと、DB上にはTaxonomyテーブルしか存在しないので、category、tag、authorのデータはtaxonomiesテーブルの中に全部ごっちゃで入っていて、どのクラスのデータかを識別するためにtypeカラムを使うって感じ。 そこで、typeカラムというRailsが標準で用意しているカラムを使用すれば、ActiveRecordがいい感じにtypeにサブクラス名を入れてくれるのです。