Syndicode
Contact Us
SYNDICODE Marketing
September 25, 2020

Manage custom fields for an ActiveRecord object in Rails

About the author:

Eduard Horiach is a Ruby on Rails Developer at Syndicode. Currently, he maintains a platform that provides water utilities and facility managers with the tools to prevent FOG (fat, oil, and grease) from entering sewer and drainage systems. Also, Eduard is passionate about building clean architecture in Rails Applications.


In this article, you can find information on developing custom fields feature for ActiveRecord objects using Postgres JSON fields and decorator patterns.

What are the custom fields?

Sometimes when we’re developing an application, we run into situations when we don’t know precisely what types of fields we need for a model or requirements. In this case, you can use custom fields.

One example might be a model for products. A product usually has fixed fields like name, description, price, and so on. But different kinds of products may have different types of fields like location to keep track of the product’s site. Or maybe you just want to add an additional_price.

The list of potential fields that you can associate with a product is endless, and creating a column for all of them in a product table will obviously not work.

These requirements can be solved using a JSON data type in PostgreSQL, which allows you to store key/value structures, just like a dictionary or hash.

What is ActiveRecord object?

Active Record in Ruby on Rails Architecture is the M in MVC – the model is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database.

Let’s Begin

Before starting, you should have bootstrapped the Rails app with Postgres as a database. I suppose you’re familiar with Ruby, Ruby on Rails, HTML, SCSS, Javascript, and Git to the start work with this tutorial. If you can’t wait to look at the ready Rails application with custom fields, check it out here.

Our goal is to create a Rails application where users can create their own shop with products that will have custom fields.

Create a shops table

Let’s start from the first step by scaffolding resources for the Shop model.

rails generate scaffold Shop name:string

*Rails scaffolding is a quick way to generate some of the major pieces of an application. If you want to create the models, views, and controllers for a new resource in a single operation, scaffolding is the tool for the job.

Change migration file for shops table in db/migrate folder to this.

class CreateShops < ActiveRecord::Migration[5.1]
 def change
   create_table :shops do |t|
     t.string :name, null: false

     t.timestamps
   end
 end
end

Add validation for the name in the Shop model.

# app/models/shop.rb
class Shop < ApplicationRecord
  validates :name, presence: true
end

Next step is a Creating a Products Table

We should create a table for products that will be extended by custom fields and belongs to the Shop.

Let’s make scaffold for products table.

rails generate scaffold Product name:string description:text price:decimal

Change code inside migration file to this.

class CreateProducts < ActiveRecord::Migration[5.1]
 def change
   create_table :products do |t|
     t.string :name
     t.text :description
     t.decimal :price
     t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }
     t.jsonb :custom_fields, null: false, default: {}, index: { using: :gin }

     t.timestamps
   end
 end
end

Let’s take a closer look in this two lines.

t.jsonb :custom_fields, null: false, default: {}, index: { using: :gin } and t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }

In the field, with the name custom_fields, we will store values of filled custom fields for a record.

Changed product model should looks like this.

 #app/models/product.rb

class Product < ApplicationRecord
  belongs_to :shop, dependent: :delete_all
  validates :name, :price, presence: true
end

Creating a table for a custom fields

Move on from the first steps of creating resources for shop and product models. Let’s create a table where we should be storing information about custom fields in our Rails application.

rails scaffold CustomField label:string internal_name:string 

Change a file with migration should look like that.

class CreateCustomFields < ActiveRecord::Migration[5.1]
 def change
   create_table :custom_fields do |t|
     t.belongs_to :shop, null: false, foreign_key: { delete: :cascade }
     t.string :label, null: false
     t.string :internal_name, null: false
     t.string :field_type, default: 0, limit: 2, null: false
     t.text :description

     t.timestamps
   end
   add_index :custom_fields, %i[internal_name], unique: true
 end
end

The next step add validation and associations to newly generated the CustomField class.

#app/models/custom_field.rb

class CustomField < ApplicationRecord
  belongs_to :shop, dependent: :delete_all
  validates :label, :internal_name, :type, presence: true
  before_validation :parameterize_internal_name

  enum field_type: {
    text: 0,
    number: 1
  }

  def parameterize_internal_name
    return if internal_name.blank?

    self.internal_name =  internal_name.parameterize(separator: '_').underscore
  end
end

Add association has_many :custom_properties for Product Model.

class Product < ApplicationRecord
  belongs_to :shop
  validates :name, :price, presence: true

  has_many :custom_properties
end

Extend product model using decorator pattern

The decorator pattern allows us to add behavior to an instance without affecting the behavior of all the instances of that class. We can “decorate” or add behavior to, a model instance before passing it to a view.

In this tutorial, we use SimpleDelegator Class that allows us to override delegated methods with super, calling the corresponding method on the wrapped object.

Let’s create Decorator for CustomField class that will display properties.

class CustomFieldDecorator < SimpleDelegator
  def initialize(object)
    super
    init_custom_accessors
  end

  protected

  def init_custom_accessors
    fields = shop.custom_fields.pluck(:internal_name)
    return if fields.empty?

    __getobj__.singleton_class.class_eval do
      store_accessor :custom_fields, *fields
    end
  end
end

Method store_accessor generates the accessor methods for Active Record Objects. Be aware that these columns use a string keyed hash and do not allow access using a symbol.

To sum up

In this tutorial, we have created the basic architecture for custom field feature for Rails Application.

Interested to know more? Stay tuned for the next part of this tutorial where we’ll write code for the controllers and views of our ActiveRecord Models and test our application in action.

Thank you for going through this tutorial with me, hope it was helpful and you like it!

Find Syndicode on GitHub and explore the example of the app 🙂