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 🙂