How can I set default values in ActiveRecord?

2019-01-02 19:10发布

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?

25条回答
何处买醉
2楼-- · 2019-01-02 19:26

I use the attribute-defaults gem

From the documentation: run sudo gem install attribute-defaults and add require 'attribute_defaults' to your app.

class Foo < ActiveRecord::Base
  attr_default :age, 18
  attr_default :last_seen do
    Time.now
  end
end

Foo.new()           # => age: 18, last_seen => "2014-10-17 09:44:27"
Foo.new(:age => 25) # => age: 25, last_seen => "2014-10-17 09:44:28"
查看更多
看风景的人
3楼-- · 2019-01-02 19:26

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?

  1. 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.

  2. 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.

  3. 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.

  4. 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:

class SellableItemFactory
  def self.new(attributes = {})
    record = Item.new(attributes)
    record.active = true if record.active.nil?
    record
  end
end

Then with this in place, the way to create a new Item would be

SellableItemFactory.new

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 = {})

查看更多
琉璃瓶的回忆
4楼-- · 2019-01-02 19:27

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?

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'.

class AddStatusToItem < ActiveRecord::Migration
  def change
    add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" }
  end
end

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:

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end
查看更多
萌妹纸的霸气范
5楼-- · 2019-01-02 19:29

Sup guys, I ended up doing the following:

def after_initialize 
 self.extras||={}
 self.other_stuff||="This stuff"
end

Works like a charm!

查看更多
与君花间醉酒
6楼-- · 2019-01-02 19:29

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end
查看更多
不再属于我。
7楼-- · 2019-01-02 19:33

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:

after_initialize :defaults

def defaults
   unless persisted?
    self.extras||={}
    self.other_stuff||="This stuff"
    self.assoc = [OtherModel.find_by_name('special')]
  end
end

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:

after_initialize :defaults, unless: :persisted?
              # ":if => :new_record?" is equivalent in this context

def defaults
  self.extras||={}
  self.other_stuff||="This stuff"
  self.assoc = [OtherModel.find_by_name('special')]
end
查看更多
登录 后发表回答