可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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?
回答1:
There are several issues with each of the available methods, but I believe that defining an after_initialize
callback is the way to go for the following reasons:
default_scope
will initialize values for new models, but then that will become the scope on which you find the model. If you just want to initialize some numbers to 0 then this is not what you want.
- Defining defaults in your migration also works part of the time... As has already been mentioned this will not work when you just call Model.new.
- Overriding
initialize
can work, but don't forget to call super
!
- Using a plugin like phusion's is getting a bit ridiculous. This is ruby, do we really need a plugin just to initialize some default values?
- Overriding
after_initialize
is deprecated as of Rails 3. When I override after_initialize
in rails 3.0.3 I get the following warning in the console:
DEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead. (called from /Users/me/myapp/app/models/my_model:15)
Therefore I'd say write an after_initialize
callback, which lets you default attributes in addition to letting you set defaults on associations like so:
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
end
Now you have just one place to look for initialization of your models. I'm using this method until someone comes up with a better one.
Caveats:
For boolean fields do:
self.bool_field = true if self.bool_field.nil?
See Paul Russell's comment on this answer for more details
If you're only selecting a subset of columns for a model (ie; using select
in a query like Person.select(:firstname, :lastname).all
) you will get a MissingAttributeError
if your init
method accesses a column that hasn't been included in the select
clause. You can guard against this case like so:
self.number ||= 0.0 if self.has_attribute? :number
and for a boolean column...
self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?
Also note that the syntax is different prior to Rails 3.2 (see Cliff Darling's comment below)
回答2:
We put the default values in the database through migrations (by specifying the :default
option on each column definition) and let Active Record use these values to set the default for each attribute.
IMHO, this approach is aligned with the principles of AR : convention over configuration, DRY, the table definition drives the model, not the other way around.
Note that the defaults are still in the application (Ruby) code, though not in the model but in the migration(s).
回答3:
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
回答4:
In Rails 5+, you can use the attribute method within your models, eg.:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
回答5:
The after_initialize callback pattern can be improved by simply doing the following
after_initialize :some_method_goes_here, :if => :new_record?
This has a non-trivial benefit if your init code needs to deal with associations, as the following code triggers a subtle n+1 if you read the initial record without including the associated.
class Account
has_one :config
after_initialize :init_config
def init_config
self.config ||= build_config
end
end
回答6:
The Phusion guys have some nice plugin for this.
回答7:
An even better/cleaner potential way than the answers proposed is to overwrite the accessor, like this:
def status
self['status'] || ACTIVE
end
See "Overwriting default accessors" in the ActiveRecord::Base documentation and more from StackOverflow on using self.
回答8:
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"
回答9:
Similar questions, but all have slightly different context:
- How do I create a default value for attributes in Rails activerecord's model?
Best Answer: Depends on What You Want!
If you want every object to start with a value: use after_initialize :init
You want the new.html
form to have a default value upon opening the page? use https://stackoverflow.com/a/5127684/1536309
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
...
end
If you want every object to have a value calculated from user input: use before_save :default_values
You want user to enter X
and then Y = X+'foo'
? use:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
回答10:
This is what constructors are for! Override the model's initialize
method.
Use the after_initialize
method.
回答11:
Sup guys, I ended up doing the following:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
Works like a charm!
回答12:
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:
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 = {})
回答13:
This has been answered for a long time, but I need default values frequently and prefer not to put them in the database. I create a DefaultValues
concern:
module DefaultValues
extend ActiveSupport::Concern
class_methods do
def defaults(attr, to: nil, on: :initialize)
method_name = "set_default_#{attr}"
send "after_#{on}", method_name.to_sym
define_method(method_name) do
if send(attr)
send(attr)
else
value = to.is_a?(Proc) ? to.call : to
send("#{attr}=", value)
end
end
private method_name
end
end
end
And then use it in my models like so:
class Widget < ApplicationRecord
include DefaultValues
defaults :category, to: 'uncategorized'
defaults :token, to: -> { SecureRandom.uuid }
end
回答14:
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
回答15:
The problem with the after_initialize solutions is that you have to add an after_initialize to every single object you look up out of the DB, regardless of whether you access this attribute or not. I suggest a lazy-loaded approach.
The attribute methods (getters) are of course methods themselves, so you can override them and provide a default. Something like:
Class Foo < ActiveRecord::Base
# has a DB column/field atttribute called 'status'
def status
(val = read_attribute(:status)).nil? ? 'ACTIVE' : val
end
end
Unless, like someone pointed out, you need to do Foo.find_by_status('ACTIVE'). In that case I think you'd really need to set the default in your database constraints, if the DB supports it.
回答16:
I ran into problems with after_initialize
giving ActiveModel::MissingAttributeError
errors when doing complex finds:
eg:
@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)
"search" in the .where
is hash of conditions
So I ended up doing it by overriding initialize in this way:
def initialize
super
default_values
end
private
def default_values
self.date_received ||= Date.current
end
The super
call is necessary to make sure the object initializing correctly from ActiveRecord::Base
before doing my customize code, ie: default_values
回答17:
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
回答18:
I strongly suggest using the "default_value_for" gem: https://github.com/FooBarWidget/default_value_for
There are some tricky scenarios that pretty much require overriding the initialize method, which that gem does.
Examples:
Your db default is NULL, your model/ruby-defined default is "some string", but you actually want to set the value to nil for whatever reason: MyModel.new(my_attr: nil)
Most solutions here will fail to set the value to nil, and will instead set it to the default.
OK, so instead of taking the ||=
approach, you switch to my_attr_changed?
...
BUT now imagine your db default is "some string", your model/ruby-defined default is "some other string", but under a certain scenario, you want to set the value to "some string" (the db default): MyModel.new(my_attr: 'some_string')
This will result in my_attr_changed?
being false because the value matches the db default, which in turn will fire your ruby-defined default code and set the value to "some other string" -- again, not what you desired.
For those reasons I don't think this can properly be accomplished with just an after_initialize hook.
Again, I think the "default_value_for" gem is taking the right approach: https://github.com/FooBarWidget/default_value_for
回答19:
Although doing that for setting default values is confusing and awkward in most cases, you can use :default_scope
as well. Check out squil's comment here.
回答20:
after_initialize method is deprecated, use the callback instead.
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
however, using :default in your migrations is still the cleanest way.
回答21:
I've found that using a validation method provides a lot of control over setting defaults. You can even set defaults (or fail validation) for updates. You even set a different default value for inserts vs updates if you really wanted to.
Note that the default won't be set until #valid? is called.
class MyModel
validate :init_defaults
private
def init_defaults
if new_record?
self.some_int ||= 1
elsif some_int.nil?
errors.add(:some_int, "can't be blank on update")
end
end
end
Regarding defining an after_initialize method, there could be performance issues because after_initialize is also called by each object returned by :find :
http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find
回答22:
If the column happens to be a 'status' type column, and your model lends itself to the use of state machines, consider using the aasm gem, after which you can simply do
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
It still doesn't initialize the value for unsaved records, but it's a bit cleaner than rolling your own with init
or whatever, and you reap the other benefits of aasm such as scopes for all your statuses.
回答23:
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
回答24:
use default_scope in rails 3
api doc
ActiveRecord obscures the difference between defaulting defined in the database (schema) and defaulting done in the application (model). During initialization, it parses the database schema and notes any default values specified there. Later, when creating objects, it assigns those schema-specified default values without touching the database.
discussion
回答25:
From the api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
Use the before_validation
method in your model, it gives you the options of creating specific initialisation for create and update calls
e.g. in this example (again code taken from the api docs example) the number field is initialised for a credit card. You can easily adapt this to set whatever values you want
class CreditCard < ActiveRecord::Base
# Strip everything but digits, so the user can specify "555 234 34" or
# "5552-3434" or both will mean "55523434"
before_validation(:on => :create) do
self.number = number.gsub(%r[^0-9]/, "") if attribute_present?("number")
end
end
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
class Firm < ActiveRecord::Base
# Destroys the associated clients and people when the firm is destroyed
before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
end
Surprised that his has not been suggested here