Rails composed_of validation
ActiveRecord allows you to declaratively write validations (e.g. validates_presence_of) in any ActiveRecord::Base models. In addition, the errors will be stored in the @model#errors collection and be used by various view helpers and methods like FormBuilder, error_messages_for, and error_message_on, wrapping a nice error CSS around the offending html form elements. But when you have a normal domain model object that is non-ActiveRecord::Base such as a composed_of type Value Object, you will not have access to these declarative validation magic methods. Today let me try to elaborate on how to use ActiveRecord::Validation methods in your composed_of objects in Rails 2.0.
(The following entry revolves around the assumption that you are using f.fields_for to create and assign value object domain models onto your ActiveRecord::Base models. Using this approach eliminates most value object creation code in your controllers, achieving "Skinny Controllers". To learn about, visit, check out the Rails composed_of &block conversion blog entry.)
Using the same example in the previous entry, let's include our ActiveRecord::Validation module into our Money class like such:
class Money
include ActiveRecord:Validations
attr_reader :balance, :currency
validates_presence_of :currency
validates_inclusion_of :currency, :in => ['USD', 'EUR'], :if => :currency?
validates_presence_of :balance
validates_numericality_of :balance, :if => :balance?
def initialize(balance, currency)
@balance, @currency = balance, currency
@errors = ActiveRecord::Errors.new self
end
def new_record?
true
end
def currency?
!@currency.blank?
end
def balance?
!@balance.blank?
end
def self.human_attribute_name(attr)
attr.humanize
end
def balance_before_type_cast
@balance
end
end
You will notice a couple things. One, I have to define the #new_record? method. This method is defined on all ActiveRecord::Base objects, but since our PORO object is not a record per se, we just stub it out. Also, we need to store a collection @errors of type ActiveRecord::Errors.
Depending on what validation routine you will end up using in Money, you may have to stub out different methods. For example, I am showing error messages with error_messages_for (more on this later), and it requires stubbing out self.human_attribute_name (as of Rails 2.0, but no longer needed in future Rails). Using validates_numericality_of requires me to stub out balance_before_type_cast. Also, the validation :if conditions requires me to add the question-mark methods balance? and currency?. Remember, this approach does not give you all the validation magic. For example, validates_uniqueness_of will not work because it assumes too much about your object being a normal AR model and needs access to a database. But in practice, your Value Objects should not need such validations, and in most cases they contain only simple one-off validations, and provide simple functionalities such as formatting like this and this.
After all these, let's see our ActiveRecord Book class.
class Book < ActiveRecord::Bases
composed_of :balance, :class_name => 'Money', :mapping => [%w(balance balance), %w(currency currency)] do |params|
Money.new params[:balance], params[:currency]
end
validates_presence_of :name
validates_associated :balance
end
The composed_of conversion block remains. You will notice the validates_associated :balance line as well. This tells your book instances that they should not be persisted should there be any balance validations failing, just like any normal validations you would write. By default, any failing balance validation will add an error message 'Balance is invalid' in your @book#errors collection. If you want to suppress that message from showing up, you can pass in option :message => nil.
So, to put it all together, here is the view and the controller code:
Name:
Balance:
Currency:
New book
To show error messages from multiple objects on the view, I am using the view helper method error_messages_for(*args). The :object option actually allows you to pass an array of objects (c'mon, you should know this trick about ActiveRecord by now. If not, check it out here and here).
def create
@book = Book.new params[:book]
if @book.save
flash[:notice] = 'Book was successfully created.'
redirect_to @book
else
render :action => "new"
end
end
Again, a skinny, thin, sexy-looking controller action.
4 comments:
Hey Chu,
Nice post.
What's this ActiveRecord::Bases thing did you mean ActiveRecord::Base?
Sammy Z
Hi Steven, this is nice work! i'm working on some statistical processing and looking at doing aggregation, but then persisting that data.
In your example, what if you wanted to keep a year-end balance?
Would you then create an AR class
class YearEndBalance < ActiveRecord::Base
:belongs_to User
# fields would be
# id, year, user_id, balance
end
and have in User.rb:
:has_many year_end_balances
Obviously, the creation of year-end-balances would be done from an off-line process from a batch-scheduler.
This would complete the full circle of data aggregation i'm trying to implement. not a very documented topic.
Stephen, you can ignore my comment above. i went through the previous 2 posts on the composed_of topic and i better understand what you did. Thanks
i am using this method with the Money gem, and their example composed_of code (http://money.rubyforge.org/). When i submit my form, i am getting undefined method `cents' for {"cents"=>"0.0", "currency"=>"usd"}:HashWithIndifferentAccess
composed_of is not looking in the amount hash for cents & currency, it is using the hash itself. i cant seem to figure out why.
Post a Comment