Class_cache has got me stumped


#1

I was working on a project this weekend and came across an issue that
hung me up for hours. The application will have an area to manage
contacts, so I have a Person model, which has_many email_addresses,
among other things. So in order to add another email_address to my
form using rjs, I send a request to the ContactMethodsController with
a the type param set to email_address. It renders the rjs, which
renders a partial and inserts it into my form. In order to accomplish
this I needed to generate a temporary ID for each email address so I
could use them in a form, generate urls with them, and when/if errors
occur the id would remain the same so the form could pick up the
errors. I do not want to save the models unless they are filled in.
This scheme works wonderfully in development, but when I push it to
production, it breaks because the generated id becomes nil after an
action other than index/new is run in the PeopleController. The index
action can be refreshed, but as soon as another action is called it
blows up.

class Person < ActiveRecord::Base

with_options :dependent => :destroy do |person|
person.has_many :phone_numbers, :as => :phoneable
person.has_many :email_addresses, :as => :emailable
person.has_many :addresses, :as => :addressable
end

def update_contact_methods(data)
result = {}
[ :email_addresses, :phone_numbers, :addresses ].collect do |c|
klass = c.to_s.classify.constantize
x = c.to_s.singularize.split(’_’)[0]

klass.transaction do
unless new_record?
  klass.destroy_all("#{x}able_id = #{self.id} AND #{x}able_type =

‘#{self.class}’")
end
result[c] = data[c].values.collect{|v|
klass.new(v) }.delete_if(&:blank?)
end
end
update_attributes(result)
end

def update_from_params(person_params)
contact_params = person_params.delete(:contact_data)

update_attributes(person_params)
reload

update_contact_methods(contact_params)

errors.empty?
end

def set_place_holder_data

give the person a temp id

self.id ||= 0

add an address it there is none

self.addresses = [ self.addresses.build ] if self.addresses.empty?

add phone numbers if we have none

self.email_addresses = [ self.email_addresses.build ] if
self.email_addresses.empty?

add phone numbers if we have none

self.phone_numbers = [ self.phone_numbers.build ] if
self.phone_numbers.empty?
end

end

Note that when set_place_holder_data is called a temporary id of 0 is
set if the id is not set and we add a dummy email_address if one does
not exist, so the fields show up on the form.

[code=]class EmailAddress < ActiveRecord::Base

Associations

belongs_to :emailable, :polymorphic => true

Attributes

cattr_reader :locations
@@locations = %w(HOME WORK OTHER)

Validation

validates_length_of :address, :within => 7…80
validates_format_of :address, :with => /^([^@\s]+)@((?:[-a-z0-9]+.)+
[a-z]{2,})$/

Class Methods
Instance Methods

def generated_identifier
@generated_identifier ||= generate_identifier
end

def id_with_generated_identifier
new_record? ? generated_identifier : id_without_generated_identifier
end
alias_method_chain :id, :generated_identifier

def blank?
address.blank?
end

private

def generate_identifier(len=10)
chars = (“a”…“z”).to_a + (1…9).to_a
identifier = “”
1.upto(len){|i| identifier += chars[rand(chars.size-1)].to_s }
identifier
end

end

Note that the EmailAddress model generates an ID for itself if it is a
new record. this is done so I can generate urls using named routes and
a dom_id for the object on a form where there may be a couple “new”
EmailAddresses. For example in a view I would:

<%= link_to "Delete", person_contact_method_url(@person, email_address) + "?type=email_address", :method => :delete %>
[/code] The dom_id would be: email_address_894j5jksl The url would be: /person/0/contact_methods/894j5jksl? type=email_address

class PeopleController < ApplicationController

helper :contact_methods

GET /people

def index

preload the new form

@person = provider.new

set place holder data for the form

@person.set_place_holder_data

get all of the contacts with eagerly loaded data

@people = provider.find_roots(:all)

respond_to do |format|
  format.html # index.rhtml
  format.xml  { render :xml => @people.to_xml }
end

end

GET /people/1

def show
@person = provider.find_with_includes(params[:id])

respond_to do |format|
  format.html # show.rhtml
  format.xml  { render :xml => @person.to_xml }
end

end

POST /people

def create
@person = provider.new

respond_to do |format|
  if @person.update_from_params(params[:person])
    flash[:notice] = 'Contact was successfully created.'
format.js {}
  else
format.js {
  render :update do |page|
  page[:new_person_form_errors].replace_html

error_messages_for(:person)
end
}
end
end
end

GET /people/1/edit

def edit
@person = provider.find_and_set_place_holders(params[:id])
end

PUT /people/1

def update
@person = provider.find(params[:id])

respond_to do |format|
  if @person.update_from_params(params[:person])
    flash[:notice] = 'Contact was successfully updated.'
format.js
    format.html { redirect_to people_url }
  else
    format.html { render :action => "edit" }
  end
end

end

private

def provider
current_account.people
end

end

class ContactMethodsController < ApplicationController

def new
@object = provider.new
respond_to do |format|
format.js
end
end

def destroy
if params[:id].match(/^\d+$/)
@object = provider.find(params[:id])
@object.destroy
end

@object = provider.new
respond_to do |format|
format.js
end
end

private

def provider
if params[:person_id].eql?(‘0’)
@person = current_account.people.new
@person.set_place_holder_data
end
@person ||= current_account.people.find(params[:person_id])

case params[:type]
when ‘phone_number’
return @person.phone_numbers
when ‘email_address’
return @person.email_addresses
when ‘address’
return @person.addresses
end
end

end

This maybe a bit confusing, but the problem occurs when I put this
code into production. The first time you load the “new” action in the
PeopleController it works fine. The second time it gives me a 500
error because the ID has disappeared from the EmailAddress and I am
using named route functions to generated urls which require it. At
first I though it had something to do with the generated_identifier
not being set the second time. Not the case, actually I put a raise
statement in the aliased method id_with_generated_identifier which
never gets called after the first execution. After hours of debugging
I figured out that if I turn off the cache_classes setting in
production/environment.rb it works great. Slow … but great. It seems
that the alias_method_chain which creates an alias from
id_with_generated_identifier to id is broken after the first
execution. I am not sure why this is. Does anyone have an idea why
this would be happening? If not, does anyone have a better way of
accomplishing this that would allow me to cache my classes again? If
more clarification is needed please let me know.

Thanks,
Nicholas