December 11, 2018
This blog is part of our Rails 5 series.
Rails 5 was a major release with a lot of new features like Action Cable, API Applications, etc. Active Record attribute API was also one of the features of Rails 5 release which did not receive much attention.
Active Record attributes API is used by Rails internally for a long time. In Rails 5 release, attributes API was made public and allowed support for custom types.
Attribute API converts the attribute value to an appropriate Ruby type. Here is how the syntax looks like.
attribute(name, cast_type, options)
The first argument is the name of the attribute and the second argument is the
cast type. Cast type can be string
, integer
or custom type object.
# db/schema.rb
create_table :movie_tickets, force: true do |t|
t.float :price
end
# without attribute API
class MovieTicket < ActiveRecord::Base
end
movie_ticket = MovieTicket.new(price: 145.40)
movie_ticket.save!
movie_ticket.price # => Float(145.40)
# with attribute API
class MovieTicket < ActiveRecord::Base
attribute :price, :integer
end
movie_ticket.price # => 145
Before using attribute API, movie ticket price was a float value, but after applying attribute on price, the price value was typecast as integer.
The database still stores the price as float and this conversion happens only in Ruby land.
Now, we will typecast movie release_date
from datetime
to date
type.
# db/schema.rb
create_table :movies, force: true do |t|
t.datetime :release_date
end
class Movie < ActiveRecord::Base
attribute :release_date, :date
end
movie.release_date # => Thu, 01 Mar 2018
We can also add default value for an attribute.
# db/schema.rb
create_table :movies, force: true do |t|
t.string :license_number, :string
end
class Movie < ActiveRecord::Base
attribute :license_number,
:string,
default: "IN00#{Date.current.strftime('%Y%m%d')}00#{rand(100)}"
end
# without attribute API with default value on license number
Movie.new.license_number # => nil
# with attribute API with default value on license number
Movie.new.license_number # => "IN00201805250068"
Let's say we want the people to rate a movie in percentage. Traditionally, we would do something like this.
class MovieRating < ActiveRecord::Base
TOTAL_STARS = 5
before_save :convert_percent_rating_to_stars
def convert_percent_rating_to_stars
rating_in_percentage = value.gsub(/\%/, '').to_f
self.rating = (rating_in_percentage * TOTAL_STARS) / 100
end
end
With attributes API we can create a custom type which will be responsible to cast to percentage rating to number of stars.
We have to define the cast
method in the custom type class which casts the
given value to the expected output.
# db/schema.rb
create_table :movie_ratings, force: true do |t|
t.integer :rating
end
# app/types/star_rating_type.rb
class StarRatingType < ActiveRecord::Type::Integer
TOTAL_STARS = 5
def cast(value)
if value.present? && !value.kind_of?(Integer)
rating_in_percentage = value.gsub(/\%/, '').to_i
star_rating = (rating_in_percentage * TOTAL_STARS) / 100
super(star_rating)
else
super
end
end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:star_rating, StarRatingType)
# app/models/movie.rb
class MovieRating < ActiveRecord::Base
attribute :rating, :star_rating
end
The attributes API also supports where
clause. Query will be converted to SQL
by calling serialize
method on the type object.
class StarRatingType < ActiveRecord::Type::Integer
TOTAL_STARS = 5
def serialize(value)
if value.present? && !value.kind_of?(Integer)
rating_in_percentage = value.gsub(/\%/, '').to_i
star_rating = (rating_in_percentage * TOTAL_STARS) / 100
super(star_rating)
else
super
end
end
end
# Add new movie rating with rating as 25.6%.
# So the movie rating in star will be 1 of 5 stars.
movie_rating = MovieRating.new(rating: "25.6%")
movie_rating.save!
movie_rating.rating # => 1
# Querying with rating in percentage 25.6%
MovieRating.where(rating: "25.6%")
# => #<ActiveRecord::Relation [#<MovieRating id: 1000, rating: 1 ... >]>
If this blog was helpful, check out our full blog archive.