You can use ActiveRecord's alias_attribute method on associations.
In an inventory system I'm working on, pallets can be assigned to various entities, such as a sales person or a sales order. Originally, they could only be reserved to a sales order, and so there's just a column in the pallets table to tie it back to a sales order record. But now, we have four such columns, and there is talk of adding several new kinds of holds. Time for a refactor!
Holds are now stored using STI in a holds table.
app/models/hold.rb
class Hold < ActiveRecord::Base
belongs_to :pallet
...
end
app/models/holds/sales_order.rb
class Holds::SalesOrder < Hold
...
end
There are types set up for each kind of hold a pallet can have, and they each have their own logic. For example, a will call fulfillment transaction can remove a number of boxes from a pallet. What if the pallet has 80 boxes, and there's an existing order that pulls 60 boxes, and the new will call order calls for 30 more? Obviously, that pallet shouldn't be used to fulfill on the order, because it doesn't have enough boxes. That kind of logic used to live in Pallet, but now it can reside more tidily in the Holds::WillCallFulfillment class.
To be able to store the STI holds classes in their own directory, I had to add the directory to the autoload_paths configuration.
config/application.rb
config.autoload_paths << File.join(Rails.root, 'app', 'models', 'holds')
I want to be able to access the "holder" object for any hold, always via the identical attribute "holder" - but sometimes it will return a sales order, sometimes a sales rep, etc.
# => returns an instance of the holding record, be it a SalesOrder, SalesRep, etc.
any_hold.holder
# => returns an instance of a SalesOrder record
any_sales_order_hold.holder
any_sales_order_hold.sales_order
# => returns an instance of a SalesRep record
any_sales_rep_hold.holder
any_sales_rep_hold.sales_rep
I want to be able to access the underlying id of the holder, stored as hold_id, by calling either Hold#hold_id or Hold#sap_sales_order_no on an instance of any derived class of Hold.
Note: because of other concerns, I opted to tie to the sap_sales_order_no attribute rather than the id.
Holds::SalesOrder.first.hold_id # => 1633
Holds::SalesOrder.first.sap_sales_order_no # => 1633
I want all the active record getter and setter methods for both the holder and the sales_order, sales_rep, etc. associations to work.
sales_order_hold = Holds::SalesOrder.new
sales_order_hold.holder = SalesOrder.find(1633)
sales_order_hold.save
sales_order_hold = Holds::SalesOrder.new
sales_order_hold.sales_order = SalesOrder.find(1633)
sales_order_hold.save
It turns out that alias_attribute works with associations, too!
class Holds::SalesOrder < Hold
belongs_to :sales_order, :class_name => "::SalesOrder", :primary_key => :sap_sales_order_no, :foreign_key => :hold_id
alias_attribute :sap_sales_order_no, :hold_id # this one is obvious
alias_attribute :holder, :sales_order # here's our "alias_association"
...
end
At first, I was mucking around with metaprogramming to make my own method, alias_association, to do this job for me. The realization that alias_attribute should work here was a ray of light.