まとめ
STIやenumに入りうる値をテーブルで持ち、外部キー制約を追加しましょう。
サンプルリポジトリ
やり方
idの型とSTIのカラムやenumのカラムの型を一致させる。
外部キー制約をはる。
db/migrate/20200627151958_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :type
t.integer :state
t.string :title
t.text :body
t.timestamps
end
create_table :post_states do |t|
t.string :name
t.timestamps
end
add_foreign_key :posts, :post_states, column: :state
create_table :post_types, id: :string do |t|
t.timestamps
end
add_foreign_key :posts, :post_types, column: :type
end
end
こうなる
db/schema.rb
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `rails
# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_06_27_160353) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "post_states", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "post_types", id: :string, force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "posts", force: :cascade do |t|
t.string "type"
t.integer "state"
t.string "title"
t.text "body"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "posts", "post_states", column: "state"
add_foreign_key "posts", "post_types", column: "type"
end
以下のような感じのSTI/enumを使ったクラス定義があるとき
app/models/post.rb
class Post < ApplicationRecord
enum state: { draft: 0, published: 1 }
end
app/models/draft_post.rb
class DraftPost < Post
end
app/models/published_post.rb
class PublishedPost < Post
end
STI/enumのカラムがとりうる値のレコードを作成しておく
app/models/post/state.rb
class Post < ApplicationRecord
class State < ApplicationRecord
class << self
def seed
Post.states.each do |state, id|
find_or_create_by!(id: id, name: state)
end
end
end
end
end
app/models/post/type.rb
class Post < ApplicationRecord
class Type < ApplicationRecord
class << self
def seed
[PublishedPost, DraftPost].each do |klass|
find_or_create_by!(id: klass.name)
end
end
end
end
end
例えばtypeカラムに存在しないクラスの名前を入れたときちゃんとエラーになって保存できない
Loading development environment (Rails 6.0.3.2)
irb(main):001:0> post = Post.first
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1 [["LIMIT", 1]]
irb(main):002:0> post
=> #<DraftPost id: 3, type: "DraftPost", state: "draft", title: "test", body: "test", created_at: "2020-06-27 16:14:49", updated_at: "2020-06-27 16:14:49">
irb(main):003:0> post.type = "YavayPost"
irb(main):004:0> post.save!
(0.3ms) BEGIN
DraftPost Update (1.2ms) UPDATE "posts" SET "type" = $1, "updated_at" = $2 WHERE "posts"."id" = $3 [["type", "YavayPost"], ["updated_at", "2020-06-27 16:40:40.492136"], ["id", 3]]
(0.2ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):4
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR: insert or update on table "posts" violates foreign key constraint "fk_rails_43c128f7b9")
DETAIL: Key (type)=(YavayPost) is not present in table "post_types".
irb(main):005:0>
またenumカラムも同様に存在しないstateを入れたときちゃんとエラーになって保存できない
irb(main):001:0> class Post; enum state: { amasawa: 4423 }; end
=> {:state=>{:amasawa=>4423}}
irb(main):002:0> post = Post.first
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1 [["LIMIT", 1]]
irb(main):003:0> post
=> #<DraftPost id: 3, type: "DraftPost", state: nil, title: "test", body: "test", created_at: "2020-06-27 16:14:49", updated_at: "2020-06-27 16:14:49">
irb(main):004:0> post.state = :amasawa
irb(main):005:0> post.save!
(0.3ms) BEGIN
DraftPost Update (1.3ms) UPDATE "posts" SET "state" = $1, "updated_at" = $2 WHERE "posts"."id" = $3 [["state", 4423], ["updated_at", "2020-06-27 16:43:03.201733"], ["id", 3]]
(0.2ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):5
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR: insert or update on table "posts" violates foreign key constraint "fk_rails_93ccb3c476")
DETAIL: Key (state)=(4423) is not present in table "post_states".
irb(main):006:0>
まとめ
データベースはべんり。
一部のinclusionも同じ手法で実装できるのでぜひやってみてください。