ActiveRecord 4.2's Type Casting
Last month Rails 4.2 was released and if you have been keeping up with my posts, I even covered how you can upgrade from 3.2 to 4.2 in one step! This speaks volumes to how easy it is to adopt to outward facing API changes within our beloved framework. But often times, version changes bring implementation changes that we never see. For example, all of Aaron Patterson's work for AdequateRecord Pro™ are performance optimizations that affect no outward API interface at all. Unless you read the source, many of these awesome changes go unnoticed and that's a damn shame because some of them can make our lives easier.
Today I want to share some of the new hotness I found while working on the latest ActiveRecord SQL Server Adapter. Specifically, how ActiveRecord type casts values. Up until Rails 4.2, all type casting was done in class methods like value_to_date
implemented on the ActiveRecord::ConnectionAdapters::Column
object. Sean Griffin does a great job explaining this convoluted process. Warning, it's kind of boring and a chore to read.
This process has been around for as long as I can remember. It made it really hard to write good abstract OO code that casts values going into and out of the database. All that has changed with the new ActiveRecord::Type
namespace. All objects within this namespace are simple POROs with very obvious and well documented interfaces. The base class is ActiveRecord::Type::Value
and below is a slightly trimmed down version of that object, sans comments. Take a quick read.
module ActiveRecord
module Type
class Value
def type_cast_from_database(value)
type_cast(value)
end
def type_cast_from_user(value)
type_cast(value)
end
def type_cast_for_database(value)
value
end
def type_cast_for_schema(value)
value.inspect
end
def changed?(old_value, new_value, _new_value_before_type_cast)
old_value != new_value
end
def changed_in_place?(*)
false
end
private
def type_cast(value)
cast_value(value) unless value.nil?
end
def cast_value(value)
value
end
end
end
end
Do you see what I see? This is amazing. I see an object that finally handles all of the following.
- Casting raw DB values.
- Casting user input to prepare for DB quoting.
- Casting for default values in schema dumpers.
- Avoid
ActiveRecord::ConnectionAdapters::Column
code bloat. - So much more!
Case in point, a lot of database connection gems still return raw strings for every value. Sub classes of Value
can define their own type_cast_from_database
implementation to deal with this. For example, here is the Integer
object's default behavior. Super easy!
def type_cast_from_database(value)
return if value.nil?
value.to_i
end
One thing that Rails core team did to make this even better allows us to type check our Ruby values ahead of time during attribute assignment vs. when we save to the database. This is now done in the Integer
class using the limit
attribute parsed from the SQL type. Here are the salient points of that class.
module ActiveRecord
module Type
class Integer < Value
def initialize(*)
super
@range = min_value...max_value
end
private
def cast_value(value)
case value
when true then 1
when false then 0
else
result = value.to_i rescue nil
ensure_in_range(result) if result
result
end
end
def ensure_in_range(value)
unless range.cover?(value)
raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
end
end
def max_value
limit = self.limit || 4
1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
end
def min_value
-max_value
end
end
end
end
Any type aliased to use the Integer
value object will now type check that the value is within the accepted database range. As far as I can tell, only Integer objects in Rails core do this, but I plan on implementing these checks for Decimal and other values too. Here is how SQL Server's smallint(2)
SQL type attribute behaves.
@obj.small_int_value = -32_768
@obj.small_int_value = -32_769 # => RangeError!
@obj.small_int_value = 32_767
@obj.small_int_value = 32_768 # => RangeError!
There is so much more that we can do with these objects. The PostgreSQL adapter already casts the JSON data type. I can even see SQL Server returning a Nokogiri object for an XML data type. The sky is the limit. The core Value
object allows the SQL Server Adapter to implement guards for different connection modes. Our TinyTDS
connection returns all DB values mapped to their proper Ruby primitive. To avoid wasting precious time, we bypass all Rails type casting in one single place now.
These objects are a great step forward and they should open up all sorts of possibilities for gems to extend our DB objects. Thanks so much to Sean Griffin and anyone else working on ActiveRecord to make it better, faster, and easier to use!