【Rails】単一テーブル継承(STI)について


単一テーブル継承(STI)とは

STI(Single Table Inheritance)

同じカラム設計のテーブルを、一つのテーブルにまとめて、継承することで余計なテーブルを増やさず、DRYなテーブル設計にするというもの。
(テーブルが多いですねw)

考え方はクラスの継承と同じ!!

STI不使用(通常テーブル設計)

Authors、categorys、tags3つとも同じカラム設計なのにテーブルをそれぞれ作成しているのは可読性が下がるし、無駄。

STI使用

図にも書いてある通り、authors、category、tagsは擬似テーブルであり、DBには実在しないテーブルになります。
実在しないので、データは全てtaxonomiesに保管されます。

この図はDBレベルでの相関図です。
Modelレベルでは、Articles.rbなどの記述を見るとわかるがAuthor、Category、Tag(Article tagsを介して)と直接アソシエーションが組まれています。
ArticleとTaxonomyは各Modelファイルを見るとアソシエーションは組まれていないことがわかります。

もう一度言いますがこれはDBレベルの相関図です。
DBとModelは切り分けて考えないと必ず混乱します。

Model

article.rb
class Article < ApplicationRecord

  belongs_to :category
  belongs_to :author

  has_many :article_tags, dependent: :destroy
  has_many :tags, through: :article_tags
end
taxonomies.rb
class Taxonomy < ApplicationRecord

end
author.rb
class Author < Taxonomy
  has_many :articles
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

擬似テーブル3つは継承元がApplicationRecordを継承したTaxonomyになっている点に注意!!

Active Record

[1] pry(main)> Article.joins(:tags).where(tags: { id: 1 }).first
  Article Load (3.5ms)  SELECT  `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_tags`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxo
nomies`.`id` = `article_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `tags`.`id` = 1 ORDER BY `articles`.`id` ASC LIMIT 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'tags.id' in 'where clause': SELECT  `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_ta
gs`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxonomies`.`id` = `article_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `tags`.`id` = 1 ORDER
 BY `articles`.`id` ASC LIMIT 1
[3] pry(main)> Article.joins(:tags).where(tags: { id: 1 })
  Article Load (2.0ms)  SELECT `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_tags`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxon
omies`.`id` = `article_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `tags`.`id` = 1
  Article Load (0.6ms)  SELECT  `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_tags`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxo
nomies`.`id` = `article_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `tags`.`id` = 1 LIMIT 11
=> #<Article::ActiveRecord_Relation:0x3fe90b9879dc>

ここでのポイントはエラーが出力されているかということ。

なぜかというと、whereは条件通りに絞り込んではくれても使用されるまでは実行されないため、エラーが出力されない。

エラー文にWHERE 'tags'の文字がある。
これはtagsテーブルからという条件のもと検索をかけているのだが、単一テーブルを継承したテーブルは擬似テーブルであり、実在しない。
実在しないテーブルから検索するのは無理なのでエラーが出ている。

[6] pry(main)> Article.joins(:tags).where(taxonomies: { id: 1 })
  Article Load (9.0ms)  SELECT `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_tags`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxon
omies`.`id` = `article_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `taxonomies`.`id` = 1
=> []

これでok

ちなみにどういうSQLが走るかだけ見たい時はto.sqlを使う。

[7] pry(main)> Article.joins(:tags).where(tags: { id: 1 }).to_sql
=> "SELECT `articles`.* FROM `articles` INNER JOIN `article_tags` ON `article_tags`.`article_id` = `articles`.`id` INNER JOIN `taxonomies` ON `taxonomies`.`id` = `artic
le_tags`.`tag_id` AND `taxonomies`.`type` IN ('Tag') WHERE `tags`.`id` = 1"

テーブル周り弱すぎなのでSQLに励みます。