Wednesday, June 17, 2009

Rails pre-2.3 and Single Form Object Graph Pains

The Problem

Recently, I was working on part of a Rails application for which I found myself needing to use polymorphic relationships and I wanted to give value objects a try.

Since the code is something under development, I will make up a simple case here. Keep in mind some goals:
  • The polymorphic child is meant to reduce a common concept to one table - so I want to have Rails help me track the type of parent using it.
  • I used value objects to decompose the polymorphic child into usable chunks - at this point I had little interest in breaking the subparts into tiny, annoying tables. Though by leveraging value objects now, I get to defer that decision all while using a healthy decomposition.
  • One form - with several subsections.
As stated in the title, I found out quickly that the recipes for using the beautiful features of Rails for form mapping, and getting all my validations either added view gunk to my model or involved using gems supporting the powerful Presenter semantic I was not ready to take on at this time.

My Approach - Do Next to Nothing

The reason for this approach is to do nothing extra to get this function done and do the right thing when I take on Rails 2.3. Of course, I may decide the Presenter is the way to go.

The above resources gave me an no immediate answer for the Value Objects - these turned out to give me various problems which I began to realize required more view supporting fake model attributes to handle param hashes for the Value Objects. Another option presented by this blog involved some stubbed out methods to make use of ActiveRecord:Validations and that made me feel like I was getting a little off track.

So I ended up with:
  • A controller managing multiple object reference variables.
  • Using a transaction block.
  • Not referring to my Value Objects in my form - but I kept them in the object because I hope to use them later.
Here is the parent class, Investor. It has a polymorphic composition: Contact Info.



class Investor < on =""> :save, :message => "can't be blank"

validates_associated :contact_info, :on => :save

composed_of :name, :mapping => [%w(contact_full_name, full_name)]

has_one :contact_info, :as => :contactable
end



Contact Info has a denormalized set of attributes - actually that is not totally true but lots of people do tend to break Phone and Address down into tiny pieces and distinct tables. I did not do so and at this point you will notice my value object for phone has one attribute in use at this time - I am planning to using the other attributes later.



class ContactInfo < on =""> :save, :message => "can't be blank"
validates_presence_of :mailing_city, :on => :save, :message => "can't be blank"
validates_presence_of :mailing_state, :on => :save, :message => "can't be blank"

validates_presence_of :primary_number, :on => :save, :message => "can't be blank"

validates_presence_of :contactable, :on => :save, :message => "can't be unvalued"


composed_of :mailing_address, :class_name => "Address", :mapping => [ %w(mailing_address1 address1), %w(mailing_city city), %w(mailing_state state), %w(mailing_zip_code zip_code), %w(mailing_address2 address2) ] do |params|
Address.new params[:mailing_address1], params[:mailing_city], params[:mailing_state], params[:mailing_zip_code], params[:mailing_address2]
end

composed_of :primary_phone, :class_name => "Phone", :mapping => [ %w(primary_number number) ] do |params|
Phone.new params[:primary_number]
end

belongs_to :contactable, :polymorphic => true
end






class Address
attr_reader :address1, :address2, :city, :state, :zip_code
def initialize(address1, city, state, zip_code, address2 = nil)
@address1, @address2, @city, @state, @zip_code = address1, address2, city, state, zip_code
end
end

class Phone
attr_reader :number, :extension
def initialize(number, extension = nil)
@number, @extension = number, extension
end
end

class Name
attr_reader :full_name
def initialize(full_name)
@full_name = full_name
end
end


As of now, I am only supporting create processing and I will show parts of the partial I use to render this data:



<% form_for @investor do |f| -%>
<div class="data_sub_section">
<p>
<%= f.label :contact_full_name, "Full Name: ", :class => "edit_required_label" %>
<%= f.text_field :contact_full_name, :class => "data_field" %>
</p>

<..... excerpt ......>

<% fields_for @contact_info do |f_contact_info| -%>
<div class="data_sub_section">
<p>
<%= f_contact_info.label :mailing_address1, "Address1: ", :class => "edit_required_label" %>
<%= f_contact_info.text_field :mailing_address1, :class => "data_field" %>
</p>

<..... excerpt ......>


Here we show both an attribute from the parent (Investor) object and the child (ContactInfo) object. We get around the whole pain with forms from using Value Objects by mapping to the attributes for the Investor and ContactInfo objects and not the Value Objects. No I don't love it, but I prefer it to the alternatives. Less code for me to write and looking to Rails 2.3 to fix this.

Okay, so you see @investor and @contact_info right? I maintain them as member variables of the investor controller. Here is the create method I use and save transaction. I took this approach to avoid saving the parent if there were errors in the child because the form treated the data as one entity - like an updateable view in a database.



def create
@investor = Investor.new(params[:investor])
begin
Investor.transaction do
@contact_info = ContactInfo.new(params[:contact_info])
@contact_info.contactable = @investor
@investor.contact_info = @contact_info
@investor.save!
end
all_valid = true
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
all_valid = false
end

respond_to do |format|
if all_valid
flash[:notice] = 'Investor was successfully created.'
format.js
format.xml { render :xml => @investor, :status => :created, :location => @investor }
else
flash[:notice] = 'Investor was not created.'
format.js
format.xml { render :xml => @investor.errors, :status => :unprocessable_entity }
end
end
end
end


I admit I still need to go back and tweak the format.xml section for @contact_info errors, my immediate need for forms to work is met. I am not overjoyed at this code, it works and I used Rails out of the box to do it.

I still think maybe Presenter is a better bet, but that is a refactoring adventure for another time.