So while working on the payment system for Codaset, I wanted to provide a little future-proofing in the form of allowing more than one method of payment. Only credit and debit cards will be supported at first, but I would like to allow payments via other methods later down the line, such as Paypal, etc. But I came across an issue that related to the fact that the database fields that would be required for credit card and paypal payments, would not be the same.
For example, for credit card payments, I would need the following fields in my payment_methods table:
create_table :payment_methods do |t|
t.integer :user_id
t.string :card_number, :card_holder
t.date :start_date, :end_date
end
But for Paypal payments, I wouldn't need any of the above fields:
create_table :payment_methods do |t|
t.integer :user_id
t.string :paypal_email
end
Now I could simply create all these fields in one table, and that would work fine, but not so elegant. I wanted a nice easy way to create and save any fields without having to create a database table column for each one. If more payment methods come along, I don't want to have to modify the database schema, by adding more columns.
So I came up with a nice little Rails plugin that lets me do exactly that.
Dynamic Attributes is a Rails plugin that lets you create dynamic attributes on any ActiveRecord model, and saves them in a schema-less fashion within a single table column. I can now save a model attribute in the usual Rails way, using any of its getters and setters, without having to actually create a table column for each attribute. And I also don't want to create a new record for each new attribute.
All I need is a single extra column in my PaymentMethod table called dynamic_attributes, and tell my model that it has dynamic attributes:
PaymentMethod < ActiveRecord::Base
has_dynamic_attributes
end
I don't even need to tell the model the names of my dynamic attributes, and I can of course mix my dynamic attributes with normal attributes.
So now I can create a new PaymentMethod record with two dynamic attributes: card_holder and card_number:
@payment_method = PaymentMethod.new :card_holder => 'Joel Moss', :card_number => '4111111111111111'
@payment_method.save
And I can call any of those dynamic attributes just like any other model attribute:
@payment_method.card_holder
=> 'Joel Moss'
@payment_method.card_number
=> '4111111111111111'
And that is pretty much it! All my dynamic attributes are indeed dynamic, and are saved in one single database column as a YAML hash. If I want to get a hash of all my dynamic attributes, I can do this:
@payment_method.dynamic_attributes
=> { :card_holder => 'Joel Moss', :card_number => '4111111111111111' }
The plugin also supports defined dynamic attributes to prevent just any old attributes being created, and you can also specify a custom database column name to save your dynamic attributes in. Read the ReadMe for more info on that, and full details of how to use the plugin with your Rails apps.
You can checkout the source on Codaset at http://codaset.com/joelmoss/dynamic-attributes. Please play with it, and let me know your thoughts. And please feel free to fork the source; your contributions are most welcome.
Nice! But I didn't want to have to specify the attributes to be used as dynamic, which my plugin lets you do. I also have no current need for associations and/or STI support.
Hey, but good job. Nice to know I'm not the only one who needed something like this.
ok, similar ideas, different purposes
Young'uns these days.. (facepalm). I feel kind of old, thinking that a new table for each new payment method would be the proper solution. There were times when data normalization and access to other applications (implemented in another language or framework) was important.
Also, suppose you want to query on your dynamic attributes. Isn't that going to be terribly expensive? Won't you need to scan your entire table, loading each row/object, unpacking the dynamic attributes, and then testing them? Sounds like a performance nightmare.
@Haig Evans-Kavaldjian: Yes you are most likely correct, but querying on dynamic attributes won't work anyway, as I do not have a need for it. I only need to be able to use it as a generic data store for each record. But if this is soemthing you need, please go ahead and fork the project and commit your changes. I would be happy to roll it into my project.
Aha, there's the rub! Time, time, never enough time! If I do find the time, though, I will be sure to share. Thanks for the offer to roll it back out. My first thought is that you could use a separate table relating existing objects to key/value pairs for each dynamic property attached to those objects. Pared down, I think you'd need four columns: primary object type, primary object id, the dynamic property name/key, and the dynamic property value. But this is a simplification that assumes both keys and values will be simple strings, when I can easily imagine wanting at the least the values to be other types, including integers and dates at least. I remember reading an article a year or two back in PHP Architect magazine, which talked about a general solution to this problem, maybe the January 2008 issue. Sadly I no longer have a copy and they haven't made their back issues available freely on the web to date. I'll cross my fingers and put this on my to do list. Thanks again.
I've done the same thing for quite some time now (3+ years), but unfortunately I made the mistake of using XML within what I called "extended_content" column.
Originally I chose XML as I thought I would simply pull the whole thing out and present it in complete form. In practice, I pulled it apart. If I was to do over again (and I couldn't use a NoSQL solution), I would opt for storing Marshalled hash in the column. Marshalling is way faster than YAML from what I have read.
However, I wouldn't even bother with that now. I would use something like a hash in Mongodb with MongoMapper instead. It really is a better solution for the "custom fields" problem.
Cheers,
Walter
I was going to use MongoDB, but I already use MySQL, and wanted to continue using MySQL while still be able to take advantage of ActiveRecord's associations, etc. So Dynamic Attributes fit the bill nicely.
Good post and this mail helped me alot in my college assignement. Gratefulness you seeking your information.
my plugin do the similar things, but offers type checking, type converters and good STI support - http://github.com/dmitryelastic/serialized_attributes