Introduction to Rails 5 Attributes
20 Dec 2015Shortly 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 transactions
exist.
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 boolean
:
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 transactions
table.
Before 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.
The 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.
Given a MoneyType
object:
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 attribute
method:
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 MoneyType
’s 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.
Type Validation
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.