Custom Attributes in Ruby on Rails 5 ActiveRecord
Lets assume the predefined ActiveRecord like
string attributes are no enough for you.
For example you would like to have a money format, roman numeral, custom time format etc. The Rails 5 provide excellent interface just for this purpose.
To demonstrate this feature we will build a model of time tracker with a custom time entries attribute. Our goal is to be able to provide time with the following format: ad bh cm, where a, b and c are integers, and d, h, m means days, hours and minutes respectively (regular expression:
((\d)+d)?\s*((\d)+h)?\s*((\d)+m)?). Eg. 3d 5h 1m means 3 days, 5 hours, 1 minute.
Lets start with a simple model
TimeEntry with only one integer field:
spent when we would like to store the spent time in minutes. The basic usage of custom attributes gives you power to change the attribute type or add default value eg:
# app/models/time_entry.rb class TimeEntry < ApplicationRecord attribute :spent, :integer, default: 0 end
Changing attribute type in our case doesn’t make any sense but think about different usages. E.g. changing the amount of completed task from
:integer where you can image the task could be partially done.
But the power of the new interface shines when the attribute should be truly custom as in our example.
Lets add a custom attribute to our class:
# app/models/time_entry.rb class TimeEntry < ApplicationRecord attribute :spent, :project_time end
Now the attribute
spent has a
:project_time type. We have to define this attribute:
# app/models/attributes/time_entry.rb class ProjectTime < ActiveRecord::Type::Integer def deserialize(value) if value.is_a?(String) to_minutes(value) else super end end def serialize(value) to_minutes(value) end private def to_minutes(time) time_sum = 0 time.split(' ').each do |time_part| value = time_part.to_i type = time_part[-1,1] case type when 'm' when 'h' value *= 60 when 'd' value *= 8*60 else value *= 60 end time_sum += value end time_sum end end
Our attribute class must derive from one of the ActiveRecord types and implement method
cast(value) which transforms provided value to derived Active Record type. In our case we perform transformation only when the provided value is a String otherwise the default integer casting is performed. Private method
to_minutes converts formatted time to an integer representing spent minutes. I assumed that 1d = 8h = 480m. E.g. result of
to_minutes('1d 1h 1m') = 541
The one final and obligatory step is to connect new
:project_time type with respective class ProjectType:
# config/initializers/type.rb ActiveRecord::Type.register(:project_time, ProjectTime)
Now you can create time entries like that:
entry = TimeEntry.create(spent: '1d 1h 1m') entry.spent #=> 541
But now thank to
serialize method we are also able to search:
TimeEntry.where(spent: '1d 1h 1m').count #=> 1 TimeEntry.where(spent: '9h 1m').count #=> 1 TimeEntry.where(spent: '1d 1h 2m').count #=> 0
You can investigate this mechanism deeper on the API documentation page: