10.19.06

Dynamically adding attributes to your model

by Chris Abad

The Situation

Let’s assume you have a form which collects data on people, but the questions you ask on the form are dynamic. The person who fills out the form is stored in one model, while the responses are stored out in another. Your migration might look something like this:

class DynamicAttributesDemo < ActiveRecord::Migration
def self.up
create_table :people do |t|
t.column :created_at, :datetime
end
create_table :responses do |t|
t.column :created_at, :datetime
t.column :question, :string
t.column :answer, :text
t.column :person_id, :integer
end
end
def self.down
drop_table :people
drop_table :responses
end
end

So each person would have their responses stored in the responses table. Initially, your models would look something like this:

class Person < ActiveRecord::Base
has_many :forms
end
class Response < ActiveRecord::Base
belongs_to :person
end

The Problem

The issue here is we want to add any responses belonging to a Person to its attribute hash. Lets take our above example and decide we want to have a form which asks for
name, email, and phone number. Not only would you be able to do this:

@person = Person.find(:first)
@person.name # Response to the name question
@person.email # Response to the email question
@person.phone_number # Response to the phone number question

but the name, email, and phone_number attributes would actually show up in the @person.attributes hash.

The Solution

Here’s how I handled this problem. I created a method which would find all of a person’s responses. It would then add each response to the person’s attribute hash, using an underscored version of the question as the key, and the answer as the value. I then use the after_find callback to add in those attributes on the fly. Pretty simple right? Here’s the code:

class Person < ActiveRecord::Base
has_many :forms
protected
def after_find
responses_to_attributes
end
def responses_to_attributes
self.responses.each do |response|
self[response.question.underscore.to_sym] = response.answer
end
end
end

The Aftermath

So there’s probably a few disclaimers I should throw in here. First of all, this obviously isn’t going to work very well for wordy questions. This isn’t very pretty:

@person.whats_your_favorite_thing_about_using_the_rails_framework

Another big disclaimer I’d like to throw out there, is you can take some significant performance hits using after_find coupled with ActiveRecord’s associations spitting out some messy SQL queries. Think of it this way: If ActiveRecord generates 20 SQL queries to find your responses that you want (not in the above example, but perhaps in an example with more complex associations), and you pull up a collection of 1,0000 ActiveRecord objects… well, you do the math. All I’m saying is you may get to the point where you need to just write a custom SQL statement for your responses_to_attributes method to try to minimize the pain.

Why?

So why go to all this trouble of pre-loading all these attributes. Why can’t we just call each attribute individually and load it at that time. It’d be a lot less stressful on the system, sure. Different people may have particular needs for the attributes hash specifically. In my case, the ToCSV plugin uses the attributes hash to generate a CSV . By loading these dynamic attributes onto the model object, I can easily export those custom questions along with their responses to a CSV file.

If you have any ideas of your own where you think this might be useful (or if you have a better way of doing this), be sure to let me know.

Digg this article

Comments

There are no comments.

Leave a Comment