How can I set default value in ActiveRecord?
I see a post from Pratik that describes an ugly, complicated chunk of code: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model
class Item < ActiveRecord::Base
def initialize_with_defaults(attrs = nil, &block)
initialize_without_defaults(attrs) do
setter = lambda { |key, value| self.send("#{key.to_s}=", value) unless
!attrs.nil? && attrs.keys.map(&:to_s).include?(key.to_s) }
setter.call('scheduler_type', 'hotseat')
yield self if block_given?
end
end
alias_method_chain :initialize, :defaults
end
I have seen the following examples googling around:
def initialize
super
self.status = ACTIVE unless self.status
end
and
def after_initialize
return unless new_record?
self.status = ACTIVE
end
I've also seen people put it in their migration, but I'd rather see it defined in the model code.
Is there a canonical way to set default value for fields in ActiveRecord model?
I use the
attribute-defaults
gemFrom the documentation: run
sudo gem install attribute-defaults
and addrequire 'attribute_defaults'
to your app.First things first: I do not disagree with Jeff's answer. It makes sense when your app is small and your logic simple. I am here trying to give an insight into how it can be a problem when building and maintaining a larger application. I do not recommend to use this approach first when building something small, but to keep it in mind as an alternative approach:
A question here is whether this default on records is business logic. If it is, I would be cautious to put it in the ORM model. Since the field ryw mentions is active, this does sound like business logic. E.g. the user is active.
Why would I be wary to put business concerns in an ORM model?
It breaks SRP. Any class inheriting from ActiveRecord::Base is already doing a lot of different things, chief among them being data consistency (validations) and persistence (save). Putting business logic, however small it is, in with AR::Base breaks SRP.
It is slower to test. If I want to test any form of logic happening in my ORM model, my tests have to initialise Rails in order to run. This wont be too much of a problem in thee beginning of your application, but will accumulate until your unit tests take a long time to run.
It will break SRP even more down the line, and in concrete ways. Say our business now requires us to email users when there Item's become active? Now we are adding email logic to the Item ORM model, whose primary responsibility is modelling an Item. It should not care about email logic. This is a case of business side effects. These do not belong in the ORM model.
It is hard to diversify. I have seen mature Rails apps with things like a database backed init_type: string field, whose only purpose is to control the initialisation logic. This is polluting the database to fix a structural problem. There are better ways, I believe.
The PORO way: While this is a bit more code, it allows you to keep your ORM Models and Business Logic separate. The code here is simplified, but should show the idea:
Then with this in place, the way to create a new Item would be
And my tests could now simply verify that ItemFactory sets active on Item if it does not have a value. No Rails initialisation needed, no SRP breaking. When Item initialisation becomes more advanced (e.g. set a status field, a default type, etc.) the ItemFactory can have this added. If we end up with two types of defaults, we can create a new BusinesCaseItemFactory to do this.
NOTE: It could also be beneficial to use dependency injection here to allow the factory to build many active things, but I left it out for simplicity. Here it is: self.new(klass = Item, attributes = {})
The canonical Rails way, before Rails 5, was actually to set it in the migration, and just look in the
db/schema.rb
for whenever wanting to see what default values are being set by the DB for any model.Contrary to what @Jeff Perrin answer states (which is a bit old), the migration approach will even apply the default when using
Model.new
, due to some Rails magic. Verified working in Rails 4.1.16.The simplest thing is often the best. Less knowledge debt and potential points of confusion in the codebase. And it 'just works'.
The
null: false
disallows NULL values in the DB, and, as an added benefit, it also updates all pre-existing DB records is set with the default value for this field as well. You may exclude this parameter in the migration if you wish, but I found it very handy!The canonical way in Rails 5+ is, as @Lucas Caton said:
Sup guys, I ended up doing the following:
Works like a charm!
https://github.com/keithrowell/rails_default_value
Some simple cases can be handled by defining a default in the database schema but that doesn't handle a number of trickier cases including calculated values and keys of other models. For these cases I do this:
I've decided to use the after_initialize but I don't want it to be applied to objects that are found only those new or created. I think it is almost shocking that an after_new callback isn't provided for this obvious use case but I've made do by confirming whether the object is already persisted indicating that it isn't new.
Having seen Brad Murray's answer this is even cleaner if the condition is moved to callback request: