単一テーブル継承(STI)とは
STI(Single Table Inheritance)
同じカラム設計のテーブルを、一つのテーブルにまとめて、継承することで余計なテーブルを増やさず、DRYなテーブル設計にするというもの。
(テーブルが多いですねw)
考え方はクラスの継承と同じ!!
例
STI不使用(通常テーブル設計)
Authors、categorys、tags
3つとも同じカラム設計なのにテーブルをそれぞれ作成しているのは可読性が下がるし、無駄。
STI使用
図にも書いてある通り、authors、category、tagsは擬似テーブルであり、DBには実在しないテーブルになります。
実在しないので、データは全てtaxonomiesに保管されます。
この図はDBレベルでの相関図です。
Modelレベルでは、Articles.rbなどの記述を見るとわかるがAuthor、Category、Tag(Article tagsを介して)と直接アソシエーションが組まれています。
ArticleとTaxonomyは各Modelファイルを見るとアソシエーションは組まれていないことがわかります。
もう一度言いますがこれはDBレベルの相関図です。
DBとModelは切り分けて考えないと必ず混乱します。
Model
class Article < ApplicationRecord
belongs_to :category
belongs_to :author
has_many :article_tags, dependent: :destroy
has_many :tags, through: :article_tags
end
class Taxonomy < ApplicationRecord
end
class Author < Taxonomy
has_many :articles
end
class Category < Taxonomy
has_many :articles
end
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に励みます。