Rails 5 Active Record attributes API

Abhay Nikam

Abhay Nikam

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.

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 ... >]>

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.