Polymorphic Associations in Rails using xid ( ids unique across the system )
On a recent project, the tech-savvy client told me we would be using xids. These are different from regular IDs in that they are a unique identifier across the domain. So, if you create a user with the ID 1000, then you create a ticket, it would have the id 1001, and then when you created some other object, it would have the id 1002.
His logic was that it would allow you to go to one central place to find any resource, if you only had the ID. Although we haven't found a use for that on the public surface of the site, it has been very useful from the polymorphic association side.
Here's what the Rails documentation shows for a polymorphic association:
Say you have a Bus, and it can either be owned by a coop, an individual, or a company.
The busses table has an owner_xid field. Since the xid field is unique, there is no need to store the type of owner. This makes me happy because it seems to extend Ruby's loose typing to the data level. You can stuff any kind of object there without extra work.
Here's how we're doing it...
belongs_to_mapped (see lib/mappable_associations.rb, below) dynamically defines the getters and setters. In this case, owner, owner=, owner_xid, owner_xid=.
Bus#owner would look something like:
* @owner keeps the object cached so you don't have to load it from the database every time you access it.
* Map is the class that hands out the XIDs. It keeps a record of the klass associated with each XID.
* Map.lookup takes any xid and returns the record it represents, raising an exception if it isn't found.
All the code is below.
As I said before, I like the idea of belongs_to_mapped. With that said, it is the only benefit we're getting from the XID system, which was implemented to solve a problem we weren't having. While it isn't high-maintenance, it's more than no-maintenance. So, unless you already have such a system in place, this probably wouldn't be a good enough reason to implement one.
the Map table:
lib/mappable_associations.rb
lib/link_to_map.rb
His logic was that it would allow you to go to one central place to find any resource, if you only had the ID. Although we haven't found a use for that on the public surface of the site, it has been very useful from the polymorphic association side.
Here's what the Rails documentation shows for a polymorphic association:
1 2 3 4 5 6 7 8 9 |
class Asset belongs_to :attachable, :polymorphic => true end class Post has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use. end @asset.attachable = @post end |
Say you have a Bus, and it can either be owned by a coop, an individual, or a company.
1 2 3 4 |
class Bus include link_to_map belongs_to_mapped :owner ... end |
The busses table has an owner_xid field. Since the xid field is unique, there is no need to store the type of owner. This makes me happy because it seems to extend Ruby's loose typing to the data level. You can stuff any kind of object there without extra work.
Here's how we're doing it...
belongs_to_mapped (see lib/mappable_associations.rb, below) dynamically defines the getters and setters. In this case, owner, owner=, owner_xid, owner_xid=.
Bus#owner would look something like:
1 2 3 |
def owner @owner ||= Map.lookup( owner_xid ) end |
* @owner keeps the object cached so you don't have to load it from the database every time you access it.
* Map is the class that hands out the XIDs. It keeps a record of the klass associated with each XID.
* Map.lookup takes any xid and returns the record it represents, raising an exception if it isn't found.
All the code is below.
As I said before, I like the idea of belongs_to_mapped. With that said, it is the only benefit we're getting from the XID system, which was implemented to solve a problem we weren't having. While it isn't high-maintenance, it's more than no-maintenance. So, unless you already have such a system in place, this probably wouldn't be a good enough reason to implement one.
the Map table:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Map #fields xid, item_type set_primary_key "xid" def klass item_type.classify.constantize end def entry klass.find xid end class < self def lookup(xid) map = find xid # raises if not found map.entry end end end |
lib/mappable_associations.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
module MappableAssociations def belongs_to_mapped(name) cache = "@#{name}" foreign_key = "#{name}_xid" define_method name do instance_variable_get(cache) || ( key = read_attribute(foreign_key) model = Map.lookup( key ) if key instance_variable_set(cache, model) ) end define_method("#{name}=") do |value| instance_variable_set(cache, value) new_xid = value ? value.xid : nil write_attribute(foreign_key, new_xid) end define_method("#{foreign_key}=") do |value| id = value.blank? ? nil : value.to_i instance_variable_set(cache, Map.lookup(id)) if id write_attribute(foreign_key, id) end define_method(foreign_key) do read_attribute(foreign_key) end define_method("save_#{name}") do instance = instance_variable_get(cache) instance.save if instance end before_save "save_#{name}" end end |
lib/link_to_map.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module LinkToMap def self.included(base) base.set_primary_key "xid" base.has_one :map, :dependent => :destroy, :foreign_key => 'xid' base.before_create :link_to_map base.extend MappableAssociations define_link_to_map end def self.define_link_to_map class_eval(<<-EOS, __FILE__, __LINE__) EOS end end |