Fusion
An extension for merging of ActiveRecord™ models.
Piggybacking
Piggybacking refers to the technique of dynamically including attributes from an associated model. This is achieved by joining the associated model in a database query and selecting the attributes that should be included with the parent object.
This is best illustrated in an example. Consider these models:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
ActiveRecord supports piggybacking simply by joining the associated table and selecting columns from it:
post = Post.select('posts.*, user.name AS user_name') \
.joins("JOIN users ON posts.user_id = users.id") \
.first
post.title # => "Why piggybacking in ActiveRecord is flawed"
post.user_name # => "Alec Smart"
As you can see, the name
attribute from User
is treated as if it were an attribute of Post
. ActiveRecord dynamically determines a model's attributes from the result set returned by the database. Every column in the result set becomes an attribute of the instantiated ActiveRecord objects. Whether the columns originate from the model's own or from a foreign table doesn't make a difference.
Or so it seems. Actually there is a drawback which becomes obvious when we select non-string columns:
post = Post.select('posts.*, user.birthday AS user_birthday, user.rating AS user_rating') \
.joins("JOIN users ON posts.user_id = users.id") \
.first
post.user_birthday # => "2011-03-01"
post. # => "4.5"
Any attributes originating from the users
table are treated as strings instead of being automatically type-casted as we would expect. The database returns result sets as plain text and ActiveRecord needs to obtain type information separately from the table schema in order to do its type-casting magic. Unfortunately, a model only knows about the columns types in its own table, so type-casting doesn't work with columns selected from foreign tables.
We could work around this by defining attribute reader methods in the Post
model that implicitly convert the values:
class Post < ActiveRecord::Base
belongs_to :user
def user_birthday
Date.parse(read_attribute(:user_birthday))
end
def
read_attribute(:user_rating).to_f
end
end
However this is tedious, error-prone and repetitive if you do it in many models. The type-casting code shown above isn't solid and would quickly become more complex in a real-life application. In its current state it won't handle nil
values properly, for example.
Fusion to the rescue!
The fusion
declaration specifies the attributes we want to fuse from associated models. Not only does it take care of the type-casting but also provides us with some additional benefits.
You simply declare which association and attributes you want to fuse:
class Post < ActiveRecord::Base
belongs_to :user
fusion :user, :only => [:name, :email, :rating, :confirmed_at]
end
Now you can do the following:
post = Post.fuse.first
post.name # => "John Doe"
post. # => 4.5
post.confirmed_at # => Tue, 01 Mar 2011
The type-casting works with any type of attribute, even with serialized ones.
Fusion uses OUTER JOIN
in order to include both records that have an associated record and ones that don't.
Of course, Fusion plays nice with Arel and you can add additional joins
, select
and other statements as you like.