3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsのSTIやenumで想定外の値が入らないようにする方法

Last updated at Posted at 2020-06-27

まとめ

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も同じ手法で実装できるのでぜひやってみてください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?