简体   繁体   中英

Rails 5 Postgres: Is there a DB constraint for inclusion?

I'm trying to ensure (as far as possible) that I have equivalent database constraints for each of my model validations. A lot of my models have a status column and I'm using the inclusion: validation to ensure the status value is in my whitelisted array.

validates :status, inclusion: { in: %w[pending active inactive] }

Is there an equivalent constraint for the DB that I can add to the create_table migration?

(Using Rails 5.2.4.1 with Postgres)

Postgres has a native enum type which is actually better than a constraint in a lot of ways.

Enumerated (enum) types are data types that comprise a static, ordered set of values. They are equivalent to the enum types supported in a number of programming languages. An example of an enum type might be the days of the week, or a set of status values for a piece of data.

This addresses one the popular criticisms of ActiveRecord::Enum integer columns which that you can't tell just from looking the database what the statuses actually mean. Its also not like just using a string column since the values are mapped to a lookup table and only occupy 4 bites on disk each.

class AddStatusToProjects < ActiveRecord::Migration[5.2]
  def up
    execute <<-SQL
      CREATE TYPE project_status AS ENUM ('pending', 'active', 'inactive');
    SQL
    add_column :project, :status, :project_status
  end

  def down
    remove_column :projects, :status
    execute <<-SQL
      DROP TYPE project_status;
    SQL
  end
end

Then setup an explicit mapping for the Enum in your model:

class Project < ApplicationRecord
  enum status: {
    'pending' => :pending,
    'active' => :active,
    'inactive' => :inactive
  }
  # ...
end

Postgres will enforce that the values are valid for the type:

irb(main):022:0> Project.create!(status: 'foo')
   (0.3ms)  BEGIN
  Project Create (3.6ms)  INSERT INTO "projects" ("created_at", "updated_at", "status") VALUES ($1, $2, $3) RETURNING "id"  [["created_at", "2020-01-16 14:15:47.579769"], ["updated_at", "2020-01-16 14:15:47.579769"], ["status", "foo"]]
   (0.4ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):22
ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR:  invalid input value for enum project_status: "foo")

The biggest con is that you need to use a SQL schema instead of a ruby schema as it cannot dump the table. But thats true for a lot of Postgres features.

If I understand you correctly, the database constraint you're looking for is either a check constraint or a foreign key constraint.

For a check constraint, you'd run SQL something like this.

alter table your_table
  add constraint constraint_name
  check (status in ('pending', 'active', 'inactive');

For a foreign key constraint, you'd first build a table of valid statuses.

create table statuses (
  status varchar(15) primary key
);
insert into statuses values 
('pending'), ('active'), ('inactive');

And in the referencing table . . .

create table your_table (
  other_columns char(1) primary key,  
  status varchar(15) not null
    references statuses (status)
);

The check constraint is more resistant to changes (you have to change the database schema to add or remove valid statuses); the foreign key is easier to change (just add or delete a row), but is more open to "tampering". Regardless of which one you use, relatively few people should have sufficient privileges to change those constraints. Really few.

But, IMHO, Rails omits a much more problematic database constraint that you probably need to think about.

This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database . . . (From Uniqueness helper documentation )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM