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.
Comments
There are no comments.
Leave a Comment