Looking for top software development talents? They are just a few clicks away.

Custom Attributes in Ruby on Rails 5 ActiveRecord

Sep 20, 20162 min read

Robert Krajewski

Co-founder and CEO of Ideamotive. Entrepreneur, mentor and startup advisor.

 

Lets assume the predefined ActiveRecord like integer or 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:

 

1
2
3
4
# 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 :decimalto :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:

 

1
2
3
4
# app/models/time_entry.rb
class TimeEntry < ApplicationRecord
attribute :spent, :integer, default: 0
end

 

Now the attribute spent has a :project_time type. We have to define this attribute:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 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:

 

1
2
# config/initializers/type.rb
ActiveRecord::Type.register(:project_time, ProjectTime)

 

Now you can create time entries like that:

 

1
2
entry = TimeEntry.create(spent: '1d 1h 1m')
entry.spent #=> 541

 

But now thank to serialize method we are also able to search:

 

1
2
3
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: http://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html

 

 

Robert Krajewski

Robert is a co-founder of Ideamotive. Entrepreneur, who with passion spreads digital revolution all around the internet. Mentor and advisor at startup accelerators. Loves to learn and discover new business models.

View all author posts
im_ebook_cover_template 4 (2)

Choosing Ruby On Rails For Your Next Web Development Project

The All-In-One Business Guide

Read now
Newsletter 9-1
Ideamotive Newsletter
Your bi-weekly collection of hottest tech news

Looking for amazing projects to work on as a Ruby on Rails developer?

Join Ideamotive Talent. Work on international projects, earn $$$, and grow your career on your terms.