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.
What is attribute API?
Attribute API converts the attribute value to an appropriate Ruby type. Here is how the syntax looks like.
1attribute(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.
1# db/schema.rb 2 3create_table :movie_tickets, force: true do |t| 4 t.float :price 5end 6 7# without attribute API 8 9class MovieTicket < ActiveRecord::Base 10end 11 12movie_ticket = MovieTicket.new(price: 145.40) 13movie_ticket.save! 14 15movie_ticket.price # => Float(145.40) 16 17# with attribute API 18 19class MovieTicket < ActiveRecord::Base 20 attribute :price, :integer 21end 22 23movie_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.
1# db/schema.rb 2 3create_table :movies, force: true do |t| 4 t.datetime :release_date 5end 6 7class Movie < ActiveRecord::Base 8 attribute :release_date, :date 9end 10 11movie.release_date # => Thu, 01 Mar 2018 12
We can also add default value for an attribute.
1# db/schema.rb 2 3create_table :movies, force: true do |t| 4 t.string :license_number, :string 5end 6 7class Movie < ActiveRecord::Base 8 attribute :license_number, 9 :string, 10 default: "IN00#{Date.current.strftime('%Y%m%d')}00#{rand(100)}" 11end 12 13# without attribute API with default value on license number 14 15Movie.new.license_number # => nil 16 17# with attribute API with default value on license number 18 19Movie.new.license_number # => "IN00201805250068"
Custom Types
Let's say we want the people to rate a movie in percentage. Traditionally, we would do something like this.
1class MovieRating < ActiveRecord::Base 2 3 TOTAL_STARS = 5 4 5 before_save :convert_percent_rating_to_stars 6 7 def convert_percent_rating_to_stars 8 rating_in_percentage = value.gsub(/\%/, '').to_f 9 10 self.rating = (rating_in_percentage * TOTAL_STARS) / 100 11 end 12end
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.
1# db/schema.rb 2 3create_table :movie_ratings, force: true do |t| 4 t.integer :rating 5end 6 7# app/types/star_rating_type.rb 8 9class StarRatingType < ActiveRecord::Type::Integer 10 TOTAL_STARS = 5 11 12 def cast(value) 13 if value.present? && !value.kind_of?(Integer) 14 rating_in_percentage = value.gsub(/\%/, '').to_i 15 16 star_rating = (rating_in_percentage * TOTAL_STARS) / 100 17 super(star_rating) 18 else 19 super 20 end 21 end 22end 23 24# config/initializers/types.rb 25 26ActiveRecord::Type.register(:star_rating, StarRatingType) 27 28# app/models/movie.rb 29 30class MovieRating < ActiveRecord::Base 31 attribute :rating, :star_rating 32end 33
Querying
The attributes API also supports where clause. Query will be converted to SQL by calling serialize method on the type object.
1class StarRatingType < ActiveRecord::Type::Integer 2 TOTAL_STARS = 5 3 4 def serialize(value) 5 if value.present? && !value.kind_of?(Integer) 6 rating_in_percentage = value.gsub(/\%/, '').to_i 7 8 star_rating = (rating_in_percentage * TOTAL_STARS) / 100 9 super(star_rating) 10 else 11 super 12 end 13 end 14end 15 16 17# Add new movie rating with rating as 25.6%. 18# So the movie rating in star will be 1 of 5 stars. 19movie_rating = MovieRating.new(rating: "25.6%") 20movie_rating.save! 21 22movie_rating.rating # => 1 23 24# Querying with rating in percentage 25.6% 25MovieRating.where(rating: "25.6%") 26 27# => #<ActiveRecord::Relation [#<MovieRating id: 1000, rating: 1 ... >]>