Rails: Using database default values for boolean attributes

Posted . Visible to the public.

In the past we validate and set default values for boolean attributes in Rails and not the database itself.

Reasons for this:

  • Older Rails didn't support database defaults when creating new records
  • Application logic is "hidden" in the database

An alternative approach, which currently reflects more the general opinion of the Rails upstream on constraints in the database, is adding default values in the schema of the database itself. We also encourage to set boolean attributes to not null. For boolean attributes with explicit three states Show archive.org snapshot you might consider using an enum instead.

create_table :users do |t|
  t.string :name
  t.boolean :locked, default: true, null: false
  t.timestamps
end

Migration

Migrating from application defaults to database is straight forward in most cases. Let's assume we have the following schema:

create_table :users do |t|
  t.string :name
  t.boolean :locked
  t.timestamps
end

The a migration might look like this:

update("UPDATE users SET locked = #{quoted_false} WHERE locked IS NULL")
change_column_null(:users, :locked, false)
change_column_default(:users, :locked, from: nil, to: false)

In case you're using the does_flag trait from the card above, you can drop

  • the does_flag trait
  • the includes in all your models

Note that the following code from the does_flag trait is only necessary in rare cases and should be avoided in general:

validates :locked, inclusion: { in: [true, false] }

has_default

In general it should be save to always use defaults in the database schema. This would make our has_default Show archive.org snapshot gem unnecessary.

There's only subtile difference: For defaults with procs Active Record inserts the class as argument and has_defaults the instance.

has_default :datetime => { Time.now } # Works
attribute :datetime, default: -> { Time.now } # Works
has_default :receive_newsletter => proc { self.role == 'customer' } # Works (`self` is an instance)
attribute :receive_newsletter, default: proc { self.role == 'customer' } # Doesn't work (`self` is the class)

Generally spoken: Default attributes based on other values of the instance are error prone. You still can achieve this by using a callback.

before_initialize :set_newsletter

private

def set_newsletter
  return unless [nil, ''].include?(receive_newsletter)

  self.receive_newsletter = (user.role == 'customer')
end

Note: When using active_type Show archive.org snapshot , the attribute method within a ActiveType::Object or aActiveType::Record will set self to the object and not the class.

Rubocop

In case you want to ensure developers don't forget to add a default value and a not null constraint, you can use the Rails/ThreeStateBooleanColumn Show archive.org snapshot cop.

Last edit
Michael Leimstädtner
License
Source code in this card is licensed under the MIT License.
Posted by Emanuel to makandra dev (2023-03-06 10:53)