Created
July 14, 2011 19:06
-
-
Save onethirtyfive/1083182 to your computer and use it in GitHub Desktop.
Class-Table Inheritance Meta-model for DataMapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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