Skip to content

Instantly share code, notes, and snippets.

@beechnut
Created April 2, 2014 22:26
Show Gist options
  • Save beechnut/9944481 to your computer and use it in GitHub Desktop.
Save beechnut/9944481 to your computer and use it in GitHub Desktop.
A First Attempt at Multi-Table Models in Rails

Multi-Table Models in Rails (without Rails for now)

I have Census datasets that store attributes in three separate files, corresponding to three separate summary levels, or spatial resolutions -- in this case, Municipalities, Census Tracts, and Census Blockgroups.

It's not the Rails way: I know. But it's not anybody else's way -- not even GeoNode has solved the problem of having attribute data separate from spatial data and joining them on the fly to one of several possible geographies.

I'd love to be able to write

class Dataset
  has_many :summary_levels
end

commute_mode = Dataset.new(name:           "Commute Mode",
                           summary_levels: [SummaryLevel.find(1), SummaryLevel.find(2), SummaryLevel.find(3)]
                           table_name:      "commute_mode")

and have the model figure out itself how to connect the dataset to its own three tables, because there's one table for each of the spatial resolutions.

There's a commute_mode_m for Municipality, commute_mode_ct for Census Tract, okay, you get me.

But for now, we can't.

So I have a superclass MultiGeo that a model could inherit from eventually (you can run this code in pure Ruby, there's presently no Rails involved). MultiGeo itself will inherit from ActiveRecord::Base.

If someone can see a path to making a multi-geo gem, please let me know.

At the moment, the has_summary_levels method checks to see if those spatial models exist, and creates by_#{summary_method} instance methods for each spatial model. Once I hook it to Rails, it should return the model's attributes joined to the spatial. Right now it outputs the SQL to be passed to #joins later on.

Also, unlike my example above, CommuteMode is its own model, not an instance of a Dataset class. I don't even know how you'd subvert Rails enough to get to a point where instances become models.

Maybe Rails really isn't right for geospatial at this level.

require 'active_support/core_ext/string/inflections'
def class_exists?(class_name)
Module.const_get(class_name.to_s.camelize)
rescue NameError
return false
end
class Municipality
class << self ; attr_accessor :table_suffix, :join_key, :table_name ; end
@table_suffix = '_m'
@join_key = 'muni_id'
@table_name = 'municipalities'
end
class CensusTract
class << self ; attr_accessor :table_suffix, :join_key, :table_name ; end
@table_suffix = '_ct'
@join_key = 'ct_id'
@table_name = 'census_tracts'
end
class CensusBlockgroup
class << self ; attr_accessor :table_suffix, :join_key, :table_name ; end
@table_suffix = '_bg'
@join_key = 'bg_id'
@table_name = 'census_blockgroups'
end
class MultiGeo
@@summary_levels = nil
def self.has_summary_levels(*args)
@@summary_levels = args
puts @@summary_levels.inspect
self.check_class_existence @@summary_levels
self.check_class_attributes @@summary_levels
@@summary_levels.each do |level|
puts "defining by_#{level}"
self.define_singleton_method("by_#{level}") do
level_class = Module.const_get(level.to_s.camelize)
spatial = level_class.table_name
data = "#{self.table_name}#{level_class.table_suffix}"
key = level_class.join_key
# Use the self.joins when we get it running in ActiveRecord
# self.joins("LEFT OUTER JOIN #{spatial} ON #{spatial}.#{key} = #{data}.#{key}")
return "LEFT OUTER JOIN #{spatial} ON #{spatial}.#{key} = #{data}.#{key}" # dummy
end
end
end
def self.check_class_existence(levels)
levels.each do |level|
puts level.inspect
raise NameError, self.exist_err_msg(level) if !class_exists?(level)
end
end
def self.check_spatial_join_attributes(klass)
if klass.join_key.nil? || klass.table_suffix.nil?
raise ArgumentError, self.arg_err_msg(level)
end
end
def self.check_class_attributes(levels)
levels.each do |level|
klass = Module.const_get(level.to_s.camelize)
check_spatial_join_attributes(klass)
end
end
def self.arg_err_msg(level)
arg_message = <<-EOM
The spatial class #{level.to_s.camelize} does not have the
required properties to be a class that relates spatially to a
MultiGeo class. It must have a `table_suffix' and a `join_key'.
EOM
end
def self.exist_err_msg(level)
exist_message = <<-EOM
The class #{level.to_s.camelize} does not exist as far as we can tell.
The arguments you pass to `has_summary_levels' must be symbol names
of classes that already exist. (Note to self: I don't know if these
will load before the summary level classes. There may be a better
way to do this.)
EOM
end
end
class CommuteMode < MultiGeo
class << self ; attr_accessor :table_name ; end
@table_name = "commute_mode"
has_summary_levels :municipality, :census_tract, :census_blockgroup
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment