h1. Datts Right bq. Dynamic Attributes Done Right Well, maybe it's done right. This gem is more of an experiment at the moment. I also did this to learn more about metaprogramming. I would like input from you guys. Is this something that is too messy? h2. Why make this? # I needed to allow users to create their own dynamic attributes on certain records. I was already running PostgreSQL. My database and code was designed largely on relational structures. I was thinking of moving to MongoDB but saw that I'd pretty much have to rewrite all the models. Creating this gem was the first step to using dynamic attributes on my app. # The "available":http://codaset.com/joelmoss/dynamic-attributes "plugins":https://github.com/moiristo/dynamic_attributes out there that did something like this stuffed the dynamic attributes in a column in the model that had dynamic attributes. Because of this, you: ** Could not order things by dynamic columns straight with SQL ** Could not (as far as I know) chain scopes, like so: @MyModel.where(:name_which_is_a_real_attribute => "Joe").where_dynamic_attribute(:phone_which_is_dynamic => "23218793")@ ** Could not find by dynamic attribute straight with SQL, like so: @MyModel.find_by_dynamic_attribute_phone_which_is_dynamic("2398291308")@ ** Had to add migrations to each model that you wanted to have dynamic attributes h2. Installation Create a migration:
class CreateDynamicAttributes < ActiveRecord::Migration def self.up create_table(:dynamic_attributes) do |t| t.string :name, :null => false t.string :attr_key, :null => false t.string :object_type, :null => false t.string :attributable_type, :null => false t.integer :attributable_id, :null => false %w(integer string boolean text float).each do |type| t.send(type, "#{type}_value".to_sym) end end add_index "dynamic_attributes", ["attributable_id"], :name => "index_dynamic_attributes_on_attributable_id" add_index "dynamic_attributes", ["attributable_type"], :name => "index_dynamic_attributes_on_attributable_type" add_index "dynamic_attributes", ["attr_key"], :name => "index_dynamic_attributes_on_attr_key" create_table(:dynamic_attribute_definitions) do |t| t.text :definition t.string :attribute_defineable_type, :null => false t.integer :attribute_defineable_id, :null => false end add_index "dynamic_attribute_definitions", ["attribute_defineable_id"], :name => "index_dynamic_attribute_definitions_on_attribute_defineable_id" add_index "dynamic_attribute_definitions", ["attribute_defineable_type"], :name => "index_dynamic_attribute_definitions_on_attribute_defineable_type" end def self.down remove_index "dynamic_attribute_definitions", :name => "index_dynamic_attribute_definitions_on_attribute_defineable_id" remove_index "dynamic_attribute_definitions", :name => "index_dynamic_attribute_definitions_on_attribute_defineable_type" drop_table(:dynamic_attribute_definitions) remove_index "dynamic_attributes", :name => "index_dynamic_attributes_on_attr_key" remove_index "dynamic_attributes", :name => "index_dynamic_attributes_on_attributable_type" remove_index "dynamic_attributes", :name => "index_dynamic_attributes_on_attributable_id" drop_table(:dynamic_attributes) end end* *attr_key* the name of the dynamic attribute * *object_type* what type of value is saved. See "object_type":#object_type. * *attributable_id* and *attributable_type* are for the polymorphic association * *integer_value*, *string_value*, *boolean_value*, *text_value*, *float_value* are used to store the data. See "object_type":#object_type. Add this to your Gemfile: @gem 'datts_right'@ Add this to the model that you want to have dynamic attributes: @has_dynamic_attributes@ h2. Usage First, you should know _when_ to use it. * Definitely more expensive than normal attributes * Can easily complicate your app if overused Knowing that, read on... h3. Adding a dynamic attribute to a record pre. @user = User.create :name => "Roland Deschain" @user.add_dynamic_attribute(:age, "integer") @user.write_dynamic_attribute :age, 820 @user.save h3. @instance.dynamic_attribute_details pre. @user.dynamic_attribute_details(:age) # Returns a dynamic attribute record, where you can access the following: @user.dynamic_attribute_details(:age).object_type # "integer" @user.dynamic_attribute_details(:age).value # 820 h3. dynamic_attribute?(:some_attribute) Returns true or false h3. Dynamic find pre. User.find_by_dynamic_attribute_age(240) # returns the first user with that age You can also use: pre. find_all_by_dynamic_attribute find_last_by_dynamic_attribute h3. Mimicking ActiveRecord You can read and write the attributes directly: @page.age = 200 # here, age is a dynamic attribute @page.age # returns 200 h3. Defining attributes If you want to define what attributes will exist, you can set an associated record to define what will attributes will automatically exist. Best explained by example:
class Category < AR::Base has_dynamic_attributes :definition => true has_many :pages end class Page < AR::Base has_dynamic_attributes :of => :category belongs_to :category endpre. @category = Category.create @category.definition = {:price => {:object_type => "integer"}, :description => {:object_type => "text"}} @category.save @page = Page.create :category => category @page.price = 200 @page.description = "hi there" What happened here was that for every definition in category, a dynamic attribute was created on every page that belongs to that category. These dynamic attributes are added to page after_create. That means if you change the definition of the category afterwards, then it won't affect the pages that already exist. The only thing that is *required* is @:price => {:object_type => "integer"}@ - you may add more information like: @:price => {:object_type => "integer", :name => "Price", :description => "How much this will cost."}@ You can access all that other information this way: pre. @page.dynamic_attribute_details(:price).definer[:name] # "Price" @page.dynamic_attribute_details(:price).definer[:description] # "How much this will cost." h3. create_defitions! You can call @create_definitions!@ on a model if you later on add the ability for that model to be a definer. For example, if you first start your Category without @has_dynamic_attributes :definition => true@, then later on you install datts_right, and you want Category to have a definition, you first add @has_dynamic_attributes :definition => true@ to Category, then you call @Category.create_definitions!@ h2. Stuff that make things faster The dynamic attributes are only actually saved when save is called on the record that has them. pre. @user.add_dynamic_attribute(:gunslinger, "boolean") # a column is already written on the "dynamic_attributes" table, with a null value. @user.add_dynamic_attribute(:middle_name, "string") # a column is already written on the "dynamic_attributes" table, with a null value. @user.write_dynamic_attribute :gunslinger, true # saves into memory @user.middle_name = "Unknown" # saves into memory @user.save # the respective dynamic attribute columns are written in the dynamic_attributes table h2. Structure of the dynamic_attributes table Although you probably shouldn't work with it directly, the dynamic_attributes table looks like this: pre. ActiveRecord::Base.connection.create_table(:dynamic_attributes) do |t| t.string :name, :null => false t.string :attr_key, :null => false t.string :object_type, :null => false t.string :attributable_type, :null => false t.integer :attributable_id, :null => false %w(integer string boolean text float).each do |type| t.send(type, "#{type}_value".to_sym) end end This means that depending on the type of dynamic attribute you're creating, things will get saved in the respective column in the dynamic_attributes table. pre. @user.add_dynamic_attribute(:gunslinger, "boolean") adds this to the dynamic_attributes table: pre. { :attr_key => "gunslinger", :attributable_id => [the user's id], :attributable_type => "User", :object_type => "boolean", :boolean_value => true } *Why have different value columns?* Because I couldn't find any other way to perform SQL operations/calculations on a generic "value" column. Besides, what kind of column would that be? A text blob? h2(#object_type). Dynamic attribute "type" Here are the different types of dynamic attributes you can save: * string * text * integer * float * boolean If you assign a value to a dynamic attribute that isn't the original type, it is ignored. pre. @user.write_dynamic_attribute :gunslinger, true @user.save @user.read_dynamic_attribute(:gunslinger) # true @user.write_dynamic_attribute :gunslinger, "hey there" @user.save @user.read_dynamic_attribute(:gunslinger) # true h2. Removing a dynamic attribute pre. @user.remove_dynamic_attribute(:gunslinger) @user.read_dynamic_attribute(:gunslinger) # NoMethodError @user.dynamic_columns # will not include :gunslinger => {...} h2. Ordering You can call pre. Product.order_by_dynamic_attribute("price", "float") # returns all users with their dynamic attribute "name" in ascending order Why pass the "float" in the order method? Because what if 2 different user records both have the dynamic attribute "price", but for one it's a float, and for the other it's an integer? How would you know which to order things by? h2. Where You can do: pre. Product.where_dynamic_attribute(:price => 200.0, :rating => 5) As of now it accepts a hash only. If you want to scope to normal attributes, use the normal @where@ method. h2. Shortcuts Pretty much all methods that have dynamic_attribute can be shorter: |_. Long name |_. Short name | | write_dynamic_attribute | write_datt | | read_dynamic_attribute | read_datt | | update_dynamic_attribute | update_datt | | update_dynamic_attributes | update_datts | | find_by_dynamic_attribute_ | find_by_datt_ | | order_by_dynamic_attribute | order_by_datt | | where_dynamic_attribute | where_datt | h2. Contributing to dynamic_attributes_right There are definitely things that don't work, and could be done better. For example, it would be nice to: # Do away with the special scopes, such as @where_dynamic_attribute@ and @order_by_dynamic_attribute@. I've tried to override ActiveRecord to make the normal @where@ and @order@ methods to see if the attributes being passed were dynamic, and do the changes necessary to the resulting SQL, but my head "started to hurt":http://stackoverflow.com/q/5590191/61018. # Have smarter caching, so that finds with dynamic attributes aren't very expensive. # Have configurable "dynamic_attributes" table Warning: the code is full of comments because I needed to visualize the code running. If you want to contribute: * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start a feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. h2. Copyright Copyright (c) 2011 Ramon Tayag. See LICENSE.txt for further details.