Introduction to Rails 5 Attributes20 Dec 2015
Shortly after the tenth anniversary of Ruby on Rails 1.0, Rails 5.0 Beta has been announced. While the main character of this release was without a doubt
ActionCable, other really great features have made their debut.
Types of Changes
One feature that particularly stood out is the introduction of
ActiveRecord Attributes. This feature allows a developer to assert a specific type for a given attribute and an optional default value. It is not strict type validation (which I have a very strong affinity for), but it does define explicit type coercion that can be very useful.
Given an example application that deals with
Transactions, a single table and model might exist:
class CreateTransactions < ActiveRecord::Migration def change create_table :transactions do |t| t.integer :user_id t.string :item_name t.integer :quantity t.string :success t.decimal :price t.timestamps(null: false) end end end class Transaction < ActiveRecord::Base end
This structure, like most in the wild, could have been created before all edge cases were thought through. For some reason,
success is a
String instead of a
Boolean. While this problem might seem trivial, imagine a system where hundreds of millions of
In such a system the entire column might not be able to change without incurring downtime. This is a perfect example problem that
Attributes can help remedy.
Starting simple, a single line can be added to the
Transaction class definition to coerce
success to a
class Transaction < ActiveRecord::Base attribute :success, :boolean end
Using the same logic that
ActiveRecord uses for database column coercion, we can see attributes in action!
transaction = Transaction.new(success: 'yes') transaction.success # => true transaction = Transaction.new(success: 'f') transaction.success # => false transaction = Transaction.new(success: 0) transaction.success # => false
Just like that, the schema has been improved. Aside from raw
SQL update statements, this code now prevents strings like
"maybe" from littering the
success column of the
Attributes, a callback or custom setter method would have been the preferred way to address this problem. However, with this new built in solution for mismatched database column types, old one-off or ad-hoc solutions can be replaced with this new standard.
The Friendly Type
With such a clean DSL, it is obvious that a lot of time and thought went into the design of
Attributes. Being able to specify how a model should interact with the persistence layer is a powerful tool.
The full list of supported types that
Attributes provides can be found deep in the source code for
ActiveRecord or right here:
#Supported Types :big_integer :binary :boolean :date :date_time :decimal :float :integer :string :text :time
Each of these types has their own place and value in a system. Their purpose is simple: to make the experience between developer and framework fluid.
Attribute feature is not restricted to database columns. If an attribute is only used during the life cycle of an object, it too can benefit from type coercion.
For instance, if a
confirmed_at attribute had a useful purpose for a
Transaction, but the format of it could vary, the
Attribute module can step in and keep things clean.
class Transaction attribute :confirmed_at, :date_time end
Without adding a new column or defining an
attr_accessor for the
confirmed_at attribute, the
:date_time coercion works perfectly:
transaction = Transaction.new transaction.confirmed_at = '2015-12-12 03:00' transaction.confirmed_at # => Sat, 12 Dec 2015 03:00:00 UTC +00:00 transaction = Transaction.new transaction.confirmed_at = '2015/12/12' transaction.confirmed_at # => Sat, 12 Dec 2015 00:00:00 UTC +00:00
Since most Ruby on Rails applications deal with strings via form encoded data, it is easy to see just how valuable the
Attribute module can be.
Out of the Box Typing
A fantastic detail about
Attributes is that type coercion is not limited to only “supported” types. Any object that adheres to a proper contract may be used.
To help illustrate this feature, we can assume that all prices in the database have been changed to only deal with cents. This helps remove some complexities with floating point math oddities and prevents nefarious Office Space/Superman III bugs from creeping up.
class MoneyType < ActiveRecord::Type::Integer def type_cast(value) if value.include?('$') price_in_dollars = value.gsub(/\$/, '').to_f price_in_dollars * 100 else value.to_i end end end
An important takeaway here is the inheritance and
type_cast method definition. Both elements are necessary to create a custom type that the
Attributes module can use effectively.
To use this new type, an initialized object is passed to the
class Transaction < ActiveRecord::Base attribute :price, MoneyType.new end
Then, if all keystrokes were done with style and grace, the effects should look something like:
Transaction.where(price: '$10.00') # => SELECT * FROM transactions WHERE price = 1000
The observed behaviour here is very interesting. A string representing a money amount can be coerced to an
Integer value representing the same data. Instead of the
type_cast method being extracted and used many times in controller or other model methods, it can be centralized to one specific spot.
Like the previous example, this solution is not the only way to achieve the desired result; however, it is unique enough to inspire new ways of thinking about these problems.
Avoiding hidden type coercion was a driving force when I created the validates_type gem. With
Attributes, that coercion is still there but can be explicitly specified in a model’s definition. While this is a great step forward, I still find value in rejecting data of the wrong type. As a previous example can illustrate: why does the string
"yes" map to the boolean value
true? It does make some sense but
"yes" is not a boolean.
The addition of
Attributes will hopefully promote some new patterns and considerations around dealing with types in Ruby on Rails applications. Data consistency and reliability are important, it is exciting to see people making steps toward it in the Rails community.