rails/rails — pull request #9460 by obrie (2013-02-27 13:25:32+00:00)
Title: Add #default_attributes for initializing new records with defaults

__TL;DR;__ It's not easy for third-party libraries to make modifications to default attribute values in Rails 4; this is a proposed solution for that problem.

__Disclaimer:__ I realize this may not be an accepted or recommended approach, but I'm throwing it out there to at least give it a shot.

__Premise:__ As a third-party library developer, I have an API that allows Rails developers to ignore the default value in the DB defined for a column and override that default through an extension on ActiveRecord models. There are a few reasons for this:

1. To remain consistent across other ORM integrations -- the same (or similar) API is available in other ORMs like Sequel, DataMapper, Mongoid, etc.
1. To remain consistent with where to define an attribute's default value for this library when the developer decides to switch to a more complex default such as one that gets determined dynamically at runtime (say based on other attributes initialized on a record). For example, you could consider allowing the default to either be a static value (a string) or a dynamic value (a proc that defines the string at runtime).
1. Although not as typical, to account for situations where the developer cannot change the DB default.

__The solutions__:

* Override #initialize and modify the `attributes` hash passed in. This introduces issues for folks that have the attributed protected from mass-assignment. If the attribute is protected, we need a different approach.
* Define an `:after_initialize` callbacks. This is too late in the initialization process; it makes it difficult, if not impossible, to know if the developer has already manually overridden the default value either through the `attributes` hash passed in to `#initialize`, default scope attributes, or the `#initialize` block. You would also want that default value defined at the same time as other column defaults in case the developer relies on the presence of that within the `#initialize` callback.
* Extend `#initialize_attributes`. This is used by more than just model initialization and is meant more as a post-processing hook, then a pre-processing hook. There are several scenarios where this would break down, especially when `nil` is the value stored in the attribute for an existing record.
* Extend and modify `#column_defaults`. This was the solution for Rails 3.x and 2.x (via `#attributes_from_column_definition`). However, in Rails 4 any modifications to `#column_defaults` get ignored because of the change to support partial inserts @ 144e8691cbfb8bba77f18cfe68d5e7fd48887f5e.
* Override `#initialize` and `#initialize_dup` to mark `#{attribute}_will_change!`. Together with the above solution (extending `#column_defaults`), *this solution works*. However, it feels dirty -- it now leaks some of the knowledge of what ActiveRecord is doing under the hood. This starts to feel more complicated than it ought to be.

So maybe the last bullet point is the right (and preferred) solution for this problem. This pull request proposes a slight refactor to some of the code in `#initialize` that makes the solution even simpler.

__Proposal__:

In this PR, the proposal is to introduce a `#default_attributes` method that gets used by `#initialize` to read the column defaults and run post-processing on those values. By extracting it out into a method, it'll allow third-party libraries to extend it further with default overrides. In addition, these changes will get written to the database on insertion in Rails 4 since they would marked as changed (from the column defaults).

The basic technique for extension would then be:

```ruby
module MyExtension
def default_attributes
defaults = super
defaults.merge!('some_attribute' => 'different_default_value')
defaults
end
end

class MyModel < ActiveRecord::Base
include MyExtension

# ...
end
```

Phew! Thanks for listening! So that's the gist of it. Thoughts?

p.s. It's also worth noting how much this would likely simplify other libraries like [default_value_for](https://github.com/FooBarWidget/default_value_for).