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.