How to dynamically add attributes to your ActiveRecord models

Published on February 10, 2017

cover picture

Sometimes we need to build an application that has domain models that we don’t know all the attributes of. A good example of such application is a system for tracking business contacts. In the center of it is a Contact model that has attributes like name, email, phone number, etc. But can we know beforehand all the attributes our Contact model will need to have? If we want to create the application for a wide audience, it can be difficult to predict. Solution? Allow users to add more attributes to Contact model in the runtime! We’ll do exactly that in this tutorial.

You can find the complete source of the demo application here. I won’t be covering the basic setup for this application (which, indeed, is very basic), and instead, focus on interesting parts.

Configuring model

First thing first, let’s install active_dynamic gem. I wrote this gem while working on the application similar to the one we’ll build in this tutorial, and the gem will do most of the heavy-lifting for us. Start by adding it to your Gemfile and running:

rails generate active_dynamic
rake db:migrate

This will copy migration and configuration files to your project and run the migration.

Next, we need to include active_dynamic into our Contact model so that the gem knows that Contact can have dynamically added attributes:

class Contact < ActiveRecord::Base
  has_dynamic_attributes

  # ... other code
end

Now we need to tell active_dynamic where to find the information about attributes it has to add to Contact model. To do that, we create a provider class that has to comply with two rules:

  • it needs to accept a model class as the only constructor argument;
  • it needs to have a call method that returns an array of attribute definitions;

Attribute definition is just a helper class that allows us to specify a display name of an attribute that will be dynamically added to our model, and, optionally, its data type, a system name, presence validation, and a default value. Here’s a simple provider class that defines age and description attributes:

class ContactAttributeProvider

  def initialize(model)
    @model = model
  end

  def call
    [
      ActiveDynamic::AttributeDefinition.new('age', datatype: ActiveDynamic::DataType::Integer, default_value: 18),
      ActiveDynamic::AttributeDefinition.new('description')
    ]
  end

end

To make them truly dynamic, we can load attribute definitions from an external storage — a database, YAML file, XML config, or something else. In the demo application, I used a contact_attributes table to store and manipulate the list of attribute definitions, so my ContactAttributeProvider looks like this:

class ContactAttributeProvider

  def initialize(model)
    @model = model
  end
  def call
    ContactAttribute.all.map do |attr|
      ActiveDynamic::AttributeDefinition.new(
        attr.name, datatype: attr.datatype,
        required: attr.required?
      )
    end
  end
end

Finally, we need to tell active_dynamic to use this provider class. In the configuration file config/initializers/active_dynamic.rb add the line:

config.provider_class = CustomAttributeResolver

Now every time an instance of Contact is created, it will have whatever attributes you listed in your attribute provider class. So now we can use it like this:

contact = Contact.new

# first_name is a regular attribute mapped to
# a column in contacts table
contact.first_name = 'John'

# age attribute was added dynamically
contact.age = 27

contact.save

# we can also do this
second_contact = Contact.new(first_name: 'Jane', age: 25)

Configuring controller and view

Now that our model is ready, let’s create a view to edit it.

In the demo application, I used simple_form gem to generate HTML forms, but the same approach can be applied using standard Rails form builder. Here is the helper method that maps an array of dynamic attributes to an array of input fields:

def dynamic_attribute_inputs(form, model)
  inputs = model.dynamic_attributes.map do |attr|
    options = {
        label: attr.display_name,
        as: datatype_mapping(attr.datatype)
    }
    form.input attr.name, options
  end

  safe_join inputs
end

def datatype_mapping(type)
  case type
    when ActiveDynamic::DataType::Text then :text
    when ActiveDynamic::DataType::Integer then :integer
    else :string
  end
end

It iterates over dynamic attributes of a model and uses datatype property that we set earlier to create different input types for different types of attributes. So it will render a text area element for text data type, numeric input for integer data type, and a simple text input for any other data type. We then can use this helper method in our view like this:

<%= simple_form_for(contact) do |f| %>
  ... code omitted
  <%= f.input :first_name %>
  <%= f.input :last_name %>
  <%= dynamic_attribute_inputs(f, contact) %>

  <%= f.button :submit %>
<% end %>

The last missing piece is in the controller. We need to white-list our dynamic attributes for mass assignment. We can do that like this:

def contact_params
  params.require(:contact).permit(
    *Contact.new.dynamic_attributes.map(&:name), :first_name)
end

Now when we create or update a Contact record, we can use contact_params hash that will include all of our dynamic attributes.

That’s it! Now we have a complete solution that allows us to add new attributes for Contact model as well as set their values without making changes in the source code.