Forum: Ruby on Rails class_cache has got me stumped

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Nicholas Barthelemy (Guest)
on 2007-07-23 23:13
(Received via mailing list)
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:

<div id ="<%= dom_id(email_address) %>">
<%= link_to "Delete", person_contact_method_url(@person,
email_address)
  + "?type=email_address", :method => :delete %>
</div>[/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
This topic is locked and can not be replied to.