Saturday, March 19, 2011

Directly access/validate underlying ActiveRecord model association record values

I recently ran into a situation where proper table normalization in my design left me with a clunky UI implementation, and no change logging. I also needed a way to directly access underlying (has-one) records for an object, through it's own accessor (think batch import). In the past, for chang logging, I've used ActiveRecord observers, or used an existing library such as Paper Trail, but I wanted this to fit into my scoped non-library dependent design and allow easy access to the underlying has-one records.

I've altered the exact implementation, the below is just an example:

Tables (users, phones) - sqlite3 syntax:

  • create table users (id integer primary key, first_name text, last_name text, created_at text, updated_at text, deleted_at text);

  • create table phones (id integer primary key, user_id integer, phone_type text, number text, created_at text, updated_at text, deleted_at text);



Models (User, Phone):


class User < ActiveRecord::Base
default_scope :conditions => { :deleted_at => nil }
has_many :phones

validates_presence_of [ :first_name, :last_name, :cell_phone ]
validates_format_of :cell_phone, :with => /\d{3}-\d{3}-\d{4}/, :if => Proc.new {|o| !o.errors.on( :cell_phone ) }

accepts_nested_attributes_for :phones

def cell_phone
@cell_phone || ( !self.phones.empty? ? self.phones.cell.first.number : nil )
end

def cell_phone=( value )
@cell_phone = value
if self.phones.cell.empty? then
self.phones.build( :phone_type => 'cell', :number => value )
else
phone = self.phones.cell.first
self.phones_attributes = [ { :id => phone.id, :number => value } ]
end
end

def destroy
self.update_attribute( :deleted_at, Time.now )
end
end


class Phone < ActiveRecord::Base
default_scope :conditions => { :deleted_at => nil }
named_scope :cell, :conditions => { :phone_type => 'cell' }

belongs_to :user

def destroy
self.update_attribute( :deleted_at, Time.now )
end
end


OK ... but what's it do?

  • Validation that plays nice with form view helpers:


    >> u = User.new( :first_name => 'Noel', :last_name => 'Geren' )
    => #
    >> u.valid?
    => false
    >> u.errors.full_messages.join(", ")
    => "Cell phone can't be blank"

  • Direct access to underlying has-one (cell phone record):


    u = User.new( :first_name => 'Noel', :last_name => 'Geren' )
    => #
    >> u.cell_phone = '123-123-1234'
    => "123-123-1234"
    >> u.cell_phone
    => "123-123-1234"