FooBarWidget / default_value_for

Provides a way to specify default values for ActiveRecord models

This URL has Read+Write access

htanata (author)
Wed Apr 22 02:19:04 -0700 2009
Hongli Lai (Phusion) (committer)
Sun May 17 05:20:07 -0700 2009
default_value_for / README.rdoc
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 1 = Introduction
2
c1d2752e » Hongli Lai (Phusion) 2008-10-03 Fix names. 3 The default_value_for plugin allows one to define default values for ActiveRecord
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 4 models in a declarative manner. For example:
5
6 class User < ActiveRecord::Base
7 default_value_for :name, "(no name)"
8 default_value_for :last_seen do
9 Time.now
10 end
11 end
12
13 u = User.new
14 u.name # => "(no name)"
15 u.last_seen # => Mon Sep 22 17:28:38 +0200 2008
16
17 *Note*: critics might be interested in the "When (not) to use default_value_for?"
18 section. Please read on.
19
20
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 21 == Installation
22
23 Install with:
24
25 ./script/plugin install git://github.com/FooBarWidget/default_value_for.git
26
27
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 28 == The default_value_for method
29
30 The +default_value_for+ method is available in all ActiveRecord model classes.
31
32 The first argument is the name of the attribute for which a default value should
33 be set. This may either be a Symbol or a String.
34
35 The default value itself may either be passed as the second argument:
36
37 default_value_for :age, 20
38
39 ...or it may be passed as the return value of a block:
40
41 default_value_for :age do
42 if today_is_sunday?
43 20
44 else
45 30
46 end
47 end
48
49 If you pass a value argument, then the default value is static and never
50 changes. However, if you pass a block, then the default value is retrieved by
51 calling the block. This block is called not once, but every time a new record is
52 instantiated and default values need to be filled in.
53
54 The latter form is especially useful if your model has a UUID column. One can
55 generate a new, random UUID for every newly instantiated record:
56
57 class User < ActiveRecord::Base
58 default_value_for :uuid do
59 UuidGenerator.new.generate_uuid
60 end
61 end
62
63 User.new.uuid # => "51d6d6846f1d1b5c9a...."
64 User.new.uuid # => "ede292289e3484cb88...."
65
66 Note that record is passed to the block as an argument, in case you need it for
67 whatever reason:
68
69 class User < ActiveRecord::Base
70 default_value_for :uuid do |x|
71 x # <--- a User object
72 UuidGenerator.new.generate_uuid
73 end
74 end
75
12653e50 » Peeja 2008-11-17 Add default_values method. 76 == The default_values method
77
78 As a shortcut, you can use +default_values+ to set multiple default values at once.
79
80 default_values :age => 20
81 :uuid => lambda { UuidGenerator.new.generate_uuid }
82
83 The difference is purely aesthetic. If you have lots of default values which are constants or constructed with one-line blocks, +default_values+ may look nicer. If you have default values constructed by longer blocks, +default_value_for+ suit you better. Feel free to mix and match.
84
85 As a side note, due to specifics of Ruby's parser, you cannot say,
86
87 default_value :uuid { UuidGenerator.new.generate_uuid }
88
89 because it will not parse. This is in part the inspiration for the +default_values+ syntax.
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 90
91 == Rules
92
93 === Instantiation of new record
94
95 Upon instantiating a new record, the declared default values are filled into
96 the record. You've already seen this in the above examples.
97
98 === Retrieval of existing record
99
100 Upon retrieving an existing record, the declared default values are _not_
101 filled into the record. Consider the example with the UUID:
102
103 user = User.create
104 user.uuid # => "529c91b8bbd3e..."
105
106 user = User.find(user.id)
107 # UUID remains unchanged because it's retrieved from the database!
108 user.uuid # => "529c91b8bbd3e..."
109
110 === Mass-assignment
111
112 If a certain attribute is being assigned via the model constructor's
113 mass-assignment argument, that the default value for that attribute will _not_
114 be filled in:
115
116 user = User.new(:uuid => "hello")
117 user.uuid # => "hello"
118
119 However, if that attribute is protected by +attr_protected+ or +attr_accessible+,
120 then it will be filled in:
121
122 class User < ActiveRecord::Base
123 default_value_for :name, 'Joe'
124 attr_protected :name
125 end
126
127 user = User.new(:name => "Jane")
128 user.name # => "Joe"
129
0c7097cc » Hongli Lai (Phusion) 2008-10-03 Fix typo. 130 === Inheritance
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 131
132 Inheritance works as expected. All default values are inherited by the child
133 class:
134
135 class User < ActiveRecord::Base
136 default_value_for :name, 'Joe'
137 end
138
139 class SuperUser < User
140 end
141
142 SuperUser.new.name # => "Joe"
143
144 === Attributes that aren't database columns
145
146 +default_value_for+ also works with attributes that aren't database columns.
147 It works with anything for which there's an assignment method:
148
149 # Suppose that your 'users' table only has a 'name' column.
150 class User < ActiveRecord::Base
151 default_value_for :name, 'Joe'
152 default_value_for :age, 20
153 default_value_for :registering, true
154
155 attr_accessor :age
156
157 def registering=(value)
158 @registering = true
159 end
160 end
161
162 user = User.new
163 user.age # => 20
164 user.instance_variable_get('@registering') # => true
165
ef531805 » Hongli Lai (Phusion) 2009-03-08 Document the fact that defa... 166 === Default values are *not* duplicated
167
168 The given default values are *not* duplicated when they are filled in, so if
169 you mutate a value that was filled in with a default value, then it will affect
170 all subsequent default values:
171
172 class Author < ActiveRecord::Base
173 # This model only has a 'name' attribute.
174 end
175
176 class Book < ActiveRecord::Base
177 belongs_to :author
178
179 # By default, a Book belongs to a new, unsaved author.
180 default_value_for :author, Author.new
181 end
182
183 book1 = Book.new
184 book1.author.name # => nil
185 # This mutates the default value:
186 book1.author.name = "John"
187
188 book2 = Book.new
189 book2.author.name # => "John"
190
191 You can prevent this from happening by passing a block to +default_value_for+,
192 which returns a new object instance every time:
193
194 class Book < ActiveRecord::Base
195 belongs_to :author
196
accb6b46 » francois 2009-04-27 Fixed syntax error in README 197 default_value_for :author do
198 Author.new
199 end
ef531805 » Hongli Lai (Phusion) 2009-03-08 Document the fact that defa... 200 end
201
202 book1 = Book.new
203 book1.author.name # => nil
204 book1.author.name = "John"
205
206 book2 = Book.new
207 book2.author.name # => nil
208
209 The main reason why default values are not duplicated is because not all
210 objects can be duplicated. For example, +Fixnum+ responds to +dup+, but calling
211 +dup+ on a Fixnum will raise an exception.
212
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 213 === Caveats
214
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 215 A conflict can occur if your model class overrides the 'initialize' method,
216 because this plugin overrides 'initialize' as well to do its job.
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 217
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 218 class User < ActiveRecord::Base
219 def initialize # <-- this constructor causes problems
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 220 super(:name => 'Name cannot be changed in constructor')
221 end
222 end
223
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 224 We recommend you to alias chain your initialize method in models where you use
225 +default_value_for+:
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 226
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 227 class User < ActiveRecord::Base
228 default_value_for :age, 20
229
230 def initialize_with_my_app
231 initialize_without_my_app(:name => 'Name cannot be changed in constructor')
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 232 end
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 233
234 alias_method_chain :initialize, :my_app
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 235 end
236
dc7a1be1 » Hongli Lai (Phusion) 2008-09-22 Update README 237 Also, stick with the following rules:
238 - There is no need to +alias_method_chain+ your initialize method in models that
239 don't use +default_value_for+.
240 - Make sure that +alias_method_chain+ is called *after* the last
241 +default_value_for+ occurance.
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 242
daa83f08 » Hongli Lai (Phusion) 2008-10-03 Fix some bugs, add more doc... 243
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 244 == When (not) to use default_value_for?
245
246 You can also specify default values in the database schema. For example, you
247 can specify a default value in a migration as follows:
248
249 create_table :users do |t|
250 t.string :username, :null => false, :default => 'default username'
251 t.integer :age, :null => false, :default => 20
252 t.timestamp :last_seen, :null => false, :default => Time.now
253 end
254
255 This has the same effect as passing the default value as the second argument to
256 +default_value_for+:
257
258 user = User.new
259 user.username # => 'default username'
260 user.age # => 20
261 user.timestamp # => Mon Sep 22 18:31:47 +0200 2008
262
263 It's recommended that you use this over +default_value_for+ whenever possible.
264
daa83f08 » Hongli Lai (Phusion) 2008-10-03 Fix some bugs, add more doc... 265 However, it's not possible to specify a schema default for serialized columns.
266 With +default_value_for+, you can:
267
268 class User < ActiveRecord::Base
269 serialize :color
270 default_value_for :color, [255, 0, 0]
271 end
272
273 And if schema defaults don't provide the flexibility that you need, then
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 274 +default_value_for+ is the perfect choice. For example, with +default_value_for+
275 you could specify a per-environment default:
276
277 class User < ActiveRecord::Base
278 if RAILS_ENV == "development"
279 default_value_for :is_admin, true
280 end
281 end
282
283 Or, as you've seen in an earlier example, you can use +default_value_for+ to
284 generate a default random UUID:
285
286 class User < ActiveRecord::Base
287 default_value_for :uuid do
288 UuidGenerator.new.generate_uuid
289 end
290 end
291
292 Or you could use it to generate a timestamp that's relative to the time at which
293 the record is instantiated:
294
295 class User < ActiveRecord::Base
296 default_value_for :account_expires_at do
297 3.years.from_now
298 end
299 end
300
301 User.new.account_expires_at # => Mon Sep 22 18:43:42 +0200 2008
302 sleep(2)
303 User.new.account_expires_at # => Mon Sep 22 18:43:44 +0200 2008
304
daa83f08 » Hongli Lai (Phusion) 2008-10-03 Fix some bugs, add more doc... 305 Finally, it's also possible to specify a default via an association:
306
307 # Has columns: 'name' and 'default_price'
308 class SuperMarket < ActiveRecord::Base
309 has_many :products
310 end
311
312 # Has columns: 'name' and 'price'
313 class Product < ActiveRecord::Base
314 belongs_to :super_market
315
316 default_value_for :price do |product|
317 product.super_market.default_price
318 end
319 end
320
321 super_market = SuperMarket.create(:name => 'Albert Zwijn', :default_price => 100)
322 soap = super_market.products.create(:name => 'Soap')
323 soap.price # => 100
324
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 325 === What about before_validate/before_save?
326
327 True, +before_validate+ and +before_save+ does what we want if we're only
328 interested in filling in a default before saving. However, if one wants to be
329 able to access the default value even before saving, then be prepared to write
330 a lot of code. Suppose that we want to be able to access a new record's UUID,
331 even before it's saved. We could end up with the following code:
332
333 # In the controller
334 def create
335 @user = User.new(params[:user])
336 @user.generate_uuid
337 email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
338 @user.save!
339 end
340
341 # Model
342 class User < ActiveRecord::Base
343 before_save :generate_uuid_if_necessary
344
345 def generate_uuid
346 self.uuid = ...
347 end
348
349 private
350 def generate_uuid_if_necessary
351 if uuid.blank?
352 generate_uuid
353 end
354 end
355 end
356
357 The need to manually call +generate_uuid+ here is ugly, and one can easily forget
358 to do that. Can we do better? Let's see:
359
360 # Controller
361 def create
362 @user = User.new(params[:user])
363 email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
364 @user.save!
365 end
366
367 # Model
368 class User < ActiveRecord::Base
369 before_save :generate_uuid_if_necessary
370
371 def uuid
372 value = read_attribute('uuid')
373 if !value
374 value = generate_uuid
375 write_attribute('uuid', value)
376 end
377 value
378 end
379
380 # We need to override this too, otherwise User.new.attributes won't return
381 # a default UUID value. I've never tested with User.create() so maybe we
382 # need to override even more things.
383 def attributes
384 uuid
385 super
386 end
387
388 private
389 def generate_uuid_if_necessary
390 uuid # Reader method automatically generates UUID if it doesn't exist
391 end
392 end
393
394 That's an awful lot of code. Using +default_value_for+ is easier, don't you think?
395
487816ed » Hongli Lai (Phusion) 2008-10-03 Update documentation: compa... 396 === What about other plugins?
397
398 I've only been able to find 2 similar plugins:
399
400 - Default Value: http://agilewebdevelopment.com/plugins/default_value
401 - ActiveRecord Defaults: http://agilewebdevelopment.com/plugins/activerecord_defaults
402
403 'Default Value' appears to be unmaintained; its SVN link is broken. This leaves
404 only 'ActiveRecord Defaults'. However, it is semantically dubious, which leaves
405 it wide open for corner cases. For example, it is not clearly specified what
406 ActiveRecord Defaults will do when attributes are protected by +attr_protected+
407 or +attr_accessible+. It is also not clearly specified what one is supposed to
408 do if one needs a custom +initialize+ method in the model.
409
d7e0eb58 » Hongli Lai (Phusion) 2008-10-03 Fix a sentence. 410 I've taken my time to thoroughly document default_value_for's behavior.
487816ed » Hongli Lai (Phusion) 2008-10-03 Update documentation: compa... 411
c9ecfe30 » Hongli Lai (Phusion) 2008-09-22 Add documentation. 412
413 == Credits
414
415 I've wanted such functionality for a while now and it baffled me that ActiveRecord
416 doesn't provide a clean way for me to specify default values. After reading
417 http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5/3e8243fa1954a935,
418 it became clear that someone needs to write a plugin. This is the result.
419
420 Thanks to Pratik Naik for providing the initial code snippet on which this plugin
421 is based on: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model