Skip to content

Instantly share code, notes, and snippets.

@onethirtyfive
Created July 14, 2011 19:06
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save onethirtyfive/1083182 to your computer and use it in GitHub Desktop.
Save onethirtyfive/1083182 to your computer and use it in GitHub Desktop.
Class-Table Inheritance Meta-model for DataMapper
# This example is an alternative to implementing polymorphic associations
# using an ActiveRecord-esque 'type' column. In this model, there are many
# 'units.' Unit is an abstract supertype. Descendants are modeled referencing
# a unit by 'unit_id', a foreign key to the primary key in the units model.
# Note: This approach maintains referential integrity in the database via
# descendants' FK to unit.id; however, it is the application's responsibility
# to ensure that no two descendants reference the same unit. This is an
# intrinsic limitation of the "Class Table Inheritance" approach, but it's
# a lot better than maintaining "type" data that is opaque to the data store.
# Units:
class Unit
include DataMapper::Resource
cattr_accessor :known_unit_types
property :id, Serial
property :name, String
has n, :unit_stats # not modeled below, used for illustration below
class << self
def type(*args)
options = args.extract_options!
unit_types = normalize(args.shift)
# Ensure specified unit types are in known_unit_types
invalid_unit_types unless (unit_types - self.known_unit_types).empty?
# Prepare descendant type query
args.unshift(:all)
unit_types.collect do |ut|
ut.to_s.camelize.constantize.send(*args)
end.flatten
end
private
def normalize(specifier)
case
when specifier === :all
self.known_unit_types
when specifier.respond_to?(:to_sym)
[specifier.to_sym]
when specifier.respond_to?(:to_ary)
specifier.to_ary
else
invalid_unit_types
end
end
def invalid_unit_types
raise ArgumentError.new('specifier should be :all, or array of valid types')
end
end
end
class Champion
include DataMapper::Resource
include Descendant
property :unit_id, Integer
property :title, String
delegate :name, :name=, :to => :unit
end
class Structure
include DataMapper::Resource
include Descendant
property :unit_id, Integer
delegate :name, :name=, :to => :unit
end
# Module with mixins for common queries across descendants:
module Descendant
# I guess I could use ActiveSupport::Concern here...
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
Unit.known_unit_types ||= []
receiver.class_eval do
cattr_accessor :unit_type
Unit.known_unit_types << (unit_type = receiver.to_s.downcase.to_sym)
end
end
module ClassMethods
def named(name)
first(unit: Unit.first(name: name))
end
end
module InstanceMethods
def unit_type
begin
self.class.unit_type
rescue NameError => e
raise 'Unit subtypes must include Descendant module'
end
end
end
end
# Usage:
@champions_and_structures = Unit.type([:champion,:structure]) # all champions and structures
@champions = Unit.type(:champion)
@specific_champion = Champion.named('Shen')
@turret = Structure.named('Turret')
# Might work?
@champions_stats = Unit.type([:champion,:structure]).collect(&:stats)
# I can't think of a good use case, but arguments passed into type will be forwarded
# to *each* Descendant class as #all options. Any further ordering, filtering, or whatever
# of the results will have to be done programmatically.
@contrived = Unit.type(:all, :conditions => [])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment