default_value_for Rails plugin: declaratively define default values for ActiveRecord models
Introduction
The default_value_for plugin allows one to define default values for ActiveRecord models in a declarative manner. For example:
class User < ActiveRecord::Base
default_value_for :name, "(no name)"
default_value_for :last_seen do
Time.now
end
end
u = User.new
u.name # => "(no name)"
u.last_seen # => Mon Sep 22 17:28:38 +0200 2008
We at Phusion use it for generating UUIDs for models.
Note: critics might be interested in the “When (not) to use default_value_for?” section. Please read on.
Installation
Install with:
./script/plugin install git://github.com/FooBarWidget/default_value_for.git
See also the AgileWebDevelopment Plugins entry.
If you like this plugin, then please consider donating and/or recommending us:
Hongli Lai
|
Ninh Bui
|
The default_value_for method
The default_value_for method is available in all ActiveRecord model classes.
The first argument is the name of the attribute for which a default value should be set. This may either be a Symbol or a String.
The default value itself may either be passed as the second argument:
default_value_for :age, 20
…or it may be passed as the return value of a block:
default_value_for :age do
if today_is_sunday?
20
else
30
end
end
If you pass a value argument, then the default value is static and never changes. However, if you pass a block, then the default value is retrieved by calling the block. This block is called not once, but every time a new record is instantiated and default values need to be filled in.
The latter form is especially useful if your model has a UUID column. One can generate a new, random UUID for every newly instantiated record:
class User < ActiveRecord::Base
default_value_for :uuid do
UuidGenerator.new.generate_uuid
end
end
User.new.uuid # => "51d6d6846f1d1b5c9a...."
User.new.uuid # => "ede292289e3484cb88...."
Note that record is passed to the block as an argument, in case you need it for whatever reason:
class User < ActiveRecord::Base
default_value_for :uuid do |x|
x # <--- a User object
UuidGenerator.new.generate_uuid
end
end
Rules
Instantiation of new record
Upon instantiating a new record, the declared default values are filled into the record. You’ve already seen this in the above examples.
Retrieval of existing record
Upon retrieving an existing record, the declared default values are not filled into the record. Consider the example with the UUID:
user = User.create user.uuid # => "529c91b8bbd3e..." user = User.find(user.id) # UUID remains unchanged because it's retrieved from the database! user.uuid # => "529c91b8bbd3e..."
Mass-assignment
If a certain attribute is being assigned via the model constructor’s mass-assignment argument, that the default value for that attribute will not be filled in:
user = User.new(:uuid => "hello") user.uuid # => "hello"
However, if that attribute is protected by attr_protected or attr_accessible, then it will be filled in:
class User < ActiveRecord::Base default_value_for :name, 'Joe' attr_protected :name end user = User.new(:name => "Jane") user.name # => "Joe"
Inheritance
Inheritance works as expected. All default values are inherited by the child
class:
class User < ActiveRecord::Base default_value_for :name, 'Joe' end class SuperUser < User end SuperUser.new.name # => "Joe"
Attributes that aren’t database columns
default_value_for also works with attributes that aren’t database columns. It works with anything for which there’s an assignment method:
# Suppose that your 'users' table only has a 'name' column.
class User < ActiveRecord::Base
default_value_for :name, 'Joe'
default_value_for :age, 20
default_value_for :registering, true
attr_accessor :age
def registering=(value)
@registering = true
end
end
user = User.new
user.age # => 20
user.instance_variable_get('@registering') # => true
Caveats
A conflict can occur if your model class overrides the ‘initialize’ method, because this plugin overrides ‘initialize’ as well to do its job.
class User < ActiveRecord::Base
def initialize # <-- this constructor causes problems
super(:name => 'Name cannot be changed in constructor')
end
end
We recommend you to alias chain your initialize method in models where you use default_value_for:
class User < ActiveRecord::Base
default_value_for :age, 20
def initialize_with_my_app
initialize_without_my_app(:name => 'Name cannot be changed in constructor')
end
alias_method_chain :initialize, :my_app
end
Also, stick with the following rules:
- There is no need to
alias_method_chainyour initialize method in models that don’t usedefault_value_for. - Make sure that
alias_method_chainis called after the lastdefault_value_foroccurance.
When (not) to use default_value_for?
You can also specify default values in the database schema. For example, you can specify a default value in a migration as follows:
create_table :users do |t| t.string :username, :null => false, :default => 'default username' t.integer :age, :null => false, :default => 20 t.timestamp :last_seen, :null => false, :default => Time.now end
This has the same effect as passing the default value as the second argument to default_value_for:
user = User.new user.username # => 'default username' user.age # => 20 user.timestamp # => Mon Sep 22 18:31:47 +0200 2008
It’s recommended that you use this over default_value_for whenever possible.
However, it’s not possible to specify a schema default for serialized columns. With default_value_for, you can:
class User < ActiveRecord::Base serialize :color default_value_for :color, [255, 0, 0] end
And if schema defaults don’t provide the flexibility that you need, then default_value_for is the perfect choice. For example, with default_value_for you could specify a per-environment default:
class User < ActiveRecord::Base
if RAILS_ENV == "development"
default_value_for :is_admin, true
end
end
Or, as you’ve seen in an earlier example, you can use default_value_for to generate a default random UUID:
class User < ActiveRecord::Base
default_value_for :uuid do
UuidGenerator.new.generate_uuid
end
end
Or you could use it to generate a timestamp that’s relative to the time at which the record is instantiated:
class User < ActiveRecord::Base
default_value_for :account_expires_at do
3.years.from_now
end
end
User.new.account_expires_at # => Mon Sep 22 18:43:42 +0200 2008
sleep(2)
User.new.account_expires_at # => Mon Sep 22 18:43:44 +0200 2008
Finally, it’s also possible to specify a default via an association:
# Has columns: 'name' and 'default_price'
class SuperMarket < ActiveRecord::Base
has_many :products
end
# Has columns: 'name' and 'price'
class Product < ActiveRecord::Base
belongs_to :super_market
default_value_for :price do |product|
product.super_market.default_price
end
end
super_market = SuperMarket.create(:name => 'Albert Zwijn', :default_price => 100)
soap = super_market.products.create(:name => 'Soap')
soap.price # => 100
What about before_validate/before_save?
True, before_validate and before_save does what we want if we’re only interested in filling in a default before saving. However, if one wants to be able to access the default value even before saving, then be prepared to write a lot of code. Suppose that we want to be able to access a new record’s UUID, even before it’s saved. We could end up with the following code:
# In the controller
def create
@user = User.new(params[:user])
@user.generate_uuid
email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
@user.save!
end
# Model
class User < ActiveRecord::Base
before_save :generate_uuid_if_necessary
def generate_uuid
self.uuid = ...
end
private
def generate_uuid_if_necessary
if uuid.blank?
generate_uuid
end
end
end
The need to manually call generate_uuid here is ugly, and one can easily forget to do that. Can we do better? Let’s see:
# Controller
def create
@user = User.new(params[:user])
email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
@user.save!
end
# Model
class User < ActiveRecord::Base
before_save :generate_uuid_if_necessary
def uuid
value = read_attribute('uuid')
if !value
value = generate_uuid
write_attribute('uuid', value)
end
value
end
# We need to override this too, otherwise User.new.attributes won't return
# a default UUID value. I've never tested with User.create() so maybe we
# need to override even more things.
def attributes
uuid
super
end
private
def generate_uuid_if_necessary
uuid # Reader method automatically generates UUID if it doesn't exist
end
end
That’s an awful lot of code. Using default_value_for is easier, don’t you think?
What about other plugins?
I’ve only been able to find 2 similar plugins:
- Default Value: http://agilewebdevelopment.com/plugins/default_value
- ActiveRecord Defaults: http://agilewebdevelopment.com/plugins/activerecord_defaults
Default Value appears to be unmaintained; its SVN link is broken. This leaves only ActiveRecord Defaults. However, it is semantically dubious, which leaves it wide open for corner cases. For example, it is not clearly specified what ActiveRecord Defaults will do when attributes are protected by attr_protected or attr_accessible. It is also not clearly specified what one is supposed to do if one needs a custom initialize method in the model.
I’ve taken my time to thoroughly document default_value_for’s behavior.
Credits
I’ve wanted such functionality for a while now and it baffled me that ActiveRecord doesn’t provide a clean way for me to specify default values. After reading http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5/3e8243fa1954a935, it became clear that someone needs to write a plugin. This is the result.
Thanks to Pratik Naik for providing the initial code snippet on which this plugin is based on: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model
If you like this plugin, then please consider donating and/or recommending us:
Hongli Lai
|
Ninh Bui
|
Hongli Lai
This is a nice way of handling things, but instead of overwriting ActiveRecord::Base#initialize, have you thought about modifying ActiveRecord::Base#after_initialize?
> However, it’s not possible to specify a schema default for serialized columns.
This isn’t entirely true. For BLOB/TEXT (text), you can’t define defaults. For VARCHAR (string), you can get away with serialization by feeding in YAML: `t.string :color, [255, 0, 0].to_yaml.chop`.
(String#chop is needed to get rid of the newline character.)
Comment by Stephen Celis — October 3, 2008 @ 10:35 am
Yeah, there have been discussions about after_initialize. But it makes too many corner case problems possible.
Comment by hongli — October 3, 2008 @ 10:42 am
Would be nice to have this in AR by default. Please consider proposing a patch for Rails. Also, isn’t it time to recommend me back on wwr?
Comment by Thijs van der Vossen — October 3, 2008 @ 11:09 am
Yes I agree, this should be in core … please consider notifying the proper authorities!
Comment by Russ Jones — October 3, 2008 @ 5:09 pm
+1 for core addition. Great work.
Comment by Jason Seifer — October 5, 2008 @ 1:10 pm
I’d vote for not including it in ActiveRecord, but that’s only because we already use something with exactly the same name and syntax at Moneyspyder
(Only joking, actually, it would be excellent to have in there.)
Our version just hooks before_validate — suits us in most situations, you can always validate the record and get the default value out (by calling valid? on it) if you really need it, but it doesn’t come up that often. We actually use default_value_for in most places — putting the defaults in the DB causes too many issues (unless things have improved since Rails 1.1). Also is easier to debug.
The default_price thing is nice, I might take that idea
— might be harder to make in a before_validation hook though.
Comment by Simon Russell — October 5, 2008 @ 4:43 pm
[...] default_value_for Rails plugin: declaratively define default values for ActiveRecord models – A plugin from Phusion. I’m a full-time Rails developer and contributor, available for long- or short-term consulting, with solid experience in working as part of a distributed team. If you’d like to hire me, drop me a line. Links [...]
Pingback by Double Shot #306 « A Fresh Cup — October 6, 2008 @ 5:18 am
[...] defaultvaluefor Rails plugin: declaratively define default values for ActiveRecord models [...]
Pingback by Nome do Jogo » Blog Archive » Rails Podcast Brasil - Episódio 33 — October 8, 2008 @ 10:46 am
[...] default_value_for Rails plugin: declaratively define default values for ActiveRecord models « Phus… [...]
Pingback by เร็วส์ หกสิบหก » นั่งเทียนเขียนข่าว#14 — October 13, 2008 @ 3:02 pm
When getting the default value via an association, like in your example, the ‘create’ method works fine, but the ‘build’ method does not fill the attribute with the desired value, leaving it nil, like so:
super_market = SuperMarket.create(:name => ‘Albert Zwijn’, :default_price => 100)
soap = super_market.products.build(:name => ‘Soap’)
soap.price # => nil
Comment by Rodrigo — October 13, 2008 @ 3:33 pm
Oops, submitted too early…
Anyway, the current workaround is to pass the attribute via the params for the build method (which should not be necessary, since this is a build via the association), like this:
super_market = SuperMarket.create(:name => ‘Albert Zwijn’, :default_price => 100)
soap = super_market.products.build(:super_market => super_market, :name => ‘Soap’)
soap.price # => 100
Comment by Rodrigo — October 13, 2008 @ 3:36 pm
Those aren’t UUIDs, those are just hex digests of big random numbers. Take a look at RFC 4122.
UUIDs look like this: 343e50b4-ba44-11dd-beb2-001ec2186a45
And they’re not just a random number. I wrote a Ruby library awhile back for generating real UUIDs, and it’s probably worth using, since it really does give you true uniqueness. Plus it runs on Ruby 1.8.x, 1.9.x, and JRuby.
http://uuidtools.rubyforge.org/api/
Your code above would become:
require "uuidtools"
class User "ee0ec316-ba44-11dd-beb2-001ec2186a45"
User.new.uuid # => "ee79dc32-ba44-11dd-beb2-001ec2186a45"
Comment by Bob Aman — November 24, 2008 @ 11:29 am
I don’t know whether this is a real bug or just a caveat, but if you call
default_value_for :foo, {}
…then the same hash is shared between instances. Perhaps a call to duping the default would work: in the mean time I am using the block form of default_value_for.
Comment by Matt Powell — December 29, 2008 @ 6:44 pm
Hi,
I use default_value in my model for students to set some inheritance:
class Student “3c03f9d9352d163861e6ba703f1e633a”, “action”=>”new”, “_method”=>”get”, “adapter”=>”_list_inline_adapter”, “controller”=>”student”}
SystemStackError (stack level too deep):
app/models/student.rb:7
vendor/plugins/default_value_for/init.rb:38:in `call’
vendor/plugins/default_value_for/init.rb:38:in `evaluate’
vendor/plugins/default_value_for/init.rb:81:in `initialize’
vendor/plugins/default_value_for/init.rb:79:in `initialize’
vendor/plugins/default_value_for/init.rb:71:in `initialize’
app/controllers/student_controller.rb:7
Rendered rescues/_trace (27.2ms)
Rendered rescues/_request_and_response (1.2ms)
Mongrel Bootup in Production mode:
=> Booting Mongrel
=> Rails 2.3.2 application starting on http://0.0.0.0:3000
/rails/formdir-dev/app/models/student.rb:7: warning: Object#id will be deprecated; use Object#object_id
/usr/lib64/ruby/gems/1.8/gems/activesupport-2.3.2/lib/active_support/dependencies.rb:443:in `load_missing_constant’: uninitialized constant Logging (NameError)
from /usr/lib64/ruby/gems/1.8/gems/activesupport-2.3.2/lib/active_support/dependencies.rb:80:in `const_missing’
from /usr/lib64/ruby/gems/1.8/gems/activesupport-2.3.2/lib/active_support/dependencies.rb:92:in `const_missing’
from /usr/lib64/ruby/gems/1.8/gems/logging-1.1.2/lib/logging.rb:479
from /usr/lib64/ruby/site_ruby/1.8/rubygems/custom_require.rb:32
/rails/formdir-dev/app/models/student.rb:8: undefined method `last_name’ for nil:NilClass (NoMethodError)
from /rails/formdir-dev/vendor/plugins/default_value_for/init.rb:38:in `call’
from /rails/formdir-dev/vendor/plugins/default_value_for/init.rb:38:in `evaluate’
from /rails/formdir-dev/vendor/plugins/default_value_for/init.rb:81:in `initialize’
from /usr/lib64/ruby/gems/1.8/gems/activesupport-2.3.2/lib/active_support/ordered_hash.rb:69:in `each’
from /usr/lib64/ruby/gems/1.8/gems/activesupport-2.3.2/lib/active_support/ordered_hash.rb:69:in `each’
from /rails/formdir-dev/vendor/plugins/default_value_for/init.rb:79:in `initialize’
from /usr/lib64/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/base.rb:2441:in `initialize_without_defaults’
from /rails/formdir-dev/vendor/plugins/default_value_for/init.rb:71:in `initialize’
… 22 levels…
from /usr/lib64/ruby/gems/1.8/gems/rails-2.3.2/lib/commands/server.rb:84
from /usr/lib64/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require’
from /usr/lib64/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `require’
from script/server:3
Comment by soren — May 15, 2009 @ 1:45 pm
Re last post
Sorry the beginning did not come out correctly:
It should have shown this:
I use default_value in my model for students to set some inheritance:
class Student < ActiveRecord::Base
belongs_to :user
belongs_to :klass
# Create programatic default values for each element in the database
default_value_for :user_id do current_user.id end
default_value_for :last_name do current_user.last_name end
default_value_for :address do current_user.address end
default_value_for :city do current_user.city end
default_value_for :zip do current_user.zip end
default_value_for :home_phone do current_user.home_phone end
default_value_for has worked fine for me under Rails 2.2.2 in Production and Development, but it fails under Rails 2.3
In Development mode it fails when the student model (per above) is used. Error message is:
(as shown in previous post)
In Production mode it prevents Mongrel from starting up. The Mongrel bootup log is shown in the previous post.
Sorry for the edit mixup
Comment by soren — May 15, 2009 @ 1:50 pm
Thanks for article. I like uuid example….
++ for core addition.
Comment by reddyonrails — July 7, 2009 @ 12:58 pm
Does it works with nested attributes?
I’m having problem with this:
class Person …)
the problem is that p.category now returns the default value.
Comment by Emma — July 28, 2009 @ 3:52 pm
Sorry the last post came out wrong.
Does it works with nested attributes?
I’m having problem with this:
class Person ...)
the problem is that p.category now returns the default value.
Comment by Emma — July 28, 2009 @ 3:54 pm
It doesn’t look like it but I’m not sure if this gets me a workaround for this
class Student < ActiveRecord::Base
has_many :klasses
def after_initialize
klasses.build(:student_id => id, :status => “registered”)
end
end
@student = Student.new
@student.klasses # <– klass id: nil, student_id: 1, status: registered
thus automatically building the klasses association right when a new student is created. Unfortunately after_initialize is also called on find which causes problems. Thus the need for a workaround.
Ya I know its a strange example. Sorry again. No preview.
Comment by Doug — August 26, 2009 @ 5:31 pm