FooBarWidget / default_value_for
- Source
- Commits
- Network (8)
- Issues (1)
- Downloads (0)
- Wiki (1)
- Graphs
-
Branch:
master
Hongli Lai (Phusion) (committer)
Sun May 17 05:20:07 -0700 2009
default_value_for / README.rdoc
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 1 | = Introduction | |
| 2 | |||||
| c1d2752e » | Hongli Lai (Phusion) | 2008-10-03 | 3 | The default_value_for plugin allows one to define default values for ActiveRecord | |
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 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 | 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 | 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 | 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 | 130 | === Inheritance | |
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 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 | 197 | default_value_for :author do | |
| 198 | Author.new | ||||
| 199 | end | ||||
| ef531805 » | Hongli Lai (Phusion) | 2009-03-08 | 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 | 213 | === Caveats | |
| 214 | |||||
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 217 | ||
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 218 | class User < ActiveRecord::Base | |
| 219 | def initialize # <-- this constructor causes problems | ||||
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 220 | super(:name => 'Name cannot be changed in constructor') | |
| 221 | end | ||||
| 222 | end | ||||
| 223 | |||||
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 226 | ||
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 232 | end | |
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 233 | ||
| 234 | alias_method_chain :initialize, :my_app | ||||
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 235 | end | |
| 236 | |||||
| dc7a1be1 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 242 | ||
| daa83f08 » | Hongli Lai (Phusion) | 2008-10-03 | 243 | ||
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | 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 | 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 | 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 | 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 | 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 | 410 | I've taken my time to thoroughly document default_value_for's behavior. | |
| 487816ed » | Hongli Lai (Phusion) | 2008-10-03 | 411 | ||
| c9ecfe30 » | Hongli Lai (Phusion) | 2008-09-22 | 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 | ||||
