During nokul we’ve heavily implemented PostgreSQL features into our Rails application. Unfortunately, ActiveRecord doesn’t come with constraint support for PostgreSQL, but rein does a fantastic job covering what’s missing in ActiveRecord. We believe that, one shouldn’t rely on a web application, that is very prone for human-error, when it comes to data integrity. Therefore our PostgreSQL tables included various constraints and limits.
Below you will find a set of rules that we’ve investigated, implemented and battle tested with various types.
Not Null Constraint & Presence Constraint
Use null: false
for foreign_key
columns, if there is no optional: true
relation in between.
t.references :unit,
null: false,
foreign_key: true
Do not use null: false
for integer
, boolean
and float
types, instead use add_null_constraint
:
add_null_constraint :students, :active
Do not use null: false
for string
type, instead use add_presence_constraint
:
add_presence_constraint :countries, :name
Not Null Constraint vs. Not Null Check
It’s possible to define NOT NULL
case as a CONSTRAINT
or as a CHECK
, in PostgreSQL. However, there are
some differences between them:
change_column_null
(ornull: false
) adds aCONSTRAINT
on a column,add_presence_constraint
adds aCHECK
to the table.
change_column_null (null: false)
change_column_null
method of Rails adds a not null
CONSTRAINT to column:
change_column_null :cities, :country_id, false
Table "public.cities"
Column | Type | Collation | Nullable | Default
--------------+------------------------+-----------+----------+------------------------------------
id | bigint | | not null | nextval('cities_id_seq'::regclass)
name | character varying(255) | | not null |
country_id | bigint | | not null |
Indexes:
"cities_pkey" PRIMARY KEY, btree (id)
"cities_name_unique" UNIQUE CONSTRAINT, btree (name) DEFERRABLE
"index_cities_on_country_id" btree (country_id)
add_null_constraint
add_null_constraint
method of rein adds a not_null
CHECK
to table:
add_null_constraint :cities, :country_id
Table "public.cities"
Column | Type | Collation | Nullable | Default
--------------+------------------------+-----------+----------+------------------------------------
id | bigint | | not null | nextval('cities_id_seq'::regclass)
name | character varying(255) | | not null |
country_id | bigint | | |
Indexes:
"cities_pkey" PRIMARY KEY, btree (id)
"cities_name_unique" UNIQUE CONSTRAINT, btree (name) DEFERRABLE
"index_cities_on_country_id" btree (country_id)
Check constraints:
"cities_country_id_null" CHECK (country_id IS NOT NULL)
PostgreSQL documentation explains how NOT NULL CHECK
and NOT NULL CONSTRAINT
are similar, but NOT NULL CONSTRAINT
is faster:
A not-null constraint is always written as a column constraint. A not-null constraint is functionally equivalent to creating a check constraint CHECK (column_name IS NOT NULL), but in PostgreSQL creating an explicit not-null constraint is more efficient.
Unfortunately the official PostgreSQL documentation doesn’t tell the performance difference in numbers. However, a Stack Overflow user mentions the performance difference as quite insignificant, around 0.5%.
add_presence_constraint
add_presence_constraint
is used to check the existence of strings. It doesn’t allow empty strings as
add_null_constraint
does:
User.create(email: ' ') # can't be created when add_presence_constraint is in place
Why CHECK
instead of CONSTRAINT
?
While the official PostgreSQL documentation mentions the performance difference between CHECK
and CONSTRAINT
in favor of CONSTRAINT
, rein still adds a CHECK
to satisfy NOT NULL
condition, simply because of two reasons:
- Reverting a
CHECK
is easy, but a whole column needs to be rewritten when reverting aCONSTRAINT
. - Since a whole column needs to be re-written when there is a change in
CONSTRAINT
, anAccessExclusiveLock
added to the table. On the other hand, changes onCHECK
doesn’t add anAccessExclusiveLock
to tables, so that they don’t cause down times.
Unique Constraint
unique_constraint
works a little bit different than the others, it adds an index:
users_email_unique
users_id_number_unique
If you want a unique_constraint
to work as the latest step of a transaction (for performance reasons),
you can also defer
it:
add_unique_constraint :books, :isbn, deferred: true