Skip to content

Instantly share code, notes, and snippets.

@bf4
Last active December 11, 2015 22:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bf4/4667296 to your computer and use it in GitHub Desktop.
Save bf4/4667296 to your computer and use it in GitHub Desktop.
Multiple table inheritance in Rails
# Multiple table inheritance in Rails
# from http://web.archive.org/web/20120406010350/http://mediumexposure.com/multiple-table-inheritance-active-record
# the below code creates a macro 'acts_as_title' that you can include in an AR model
# including 'acts_as_title' adds the attributes from the TitleProperties model/table
# so that, e.g. a TvTitle can have attributes that are a combination
# of the tv_titles table and the title_properties table
# and behaves more or less as a normal AR object.
#
# yeah this looks a little nuts and needs refactoring
#config/initializers/acts_as_title.rb
class ActiveRecord::Base
def self.acts_as_title
include ActsAsTitle
end
end
#app/models/title_properties
class TitleProperties < ActiveRecord::Base
self.table_name = 'titles'
belongs_to :acts_as_title, :polymorphic => true, :dependent => :destroy
end
class AddTitleTypeToTitleTable < ActiveRecord::Migration
def change
add_column :titles, :title_type, :string, :null => false
add_column :titles, :title_id, :integer, :null => false
add_index :titles, [:title_id, :title_type]
end
end
#app/models/tv_title.rb
class TvTitle < ActiveRecord::Base
acts_as_title
end
#app/models/acts_as_title.rb
module ActsAsTitle
def self.included(base)
base.has_one :title_properties, :as => :title, :autosave => true, :dependent => :destroy
base.validate :title_properties_must_be_valid
base.alias_method_chain :title_properties, :autobuild
base.extend ClassMethods
base.define_title_properties_accessors
base.define_nested_attributes_accessors
base.define_title_properties_associations_accessors
base.mix_in_title_properties_attributes
end
def title_properties_with_autobuild
title_properties_without_autobuild || build_title_properties
end
def method_missing(meth, *args, &blk)
title_properties.send(meth, *args, &blk)
rescue NoMethodError
super
end
module ClassMethods
def define_title_properties_accessors
all_attributes = TitleProperties.content_columns.map(&:name)
ignored_attributes = ["created_at", "updated_at", "title_type"]
attributes_to_delegate = all_attributes - ignored_attributes
attributes_to_delegate.each do |attrib|
class_eval <<-RUBY
def #{attrib}
title_properties.#{attrib}
end
def #{attrib}=(value)
self.title_properties.#{attrib} = value
end
def #{attrib}?
self.title_properties.#{attrib}?
end
RUBY
end
end
def define_nested_attributes_accessors
TitleProperties.nested_attributes_options.keys.each do |attrib|
module_eval <<-RUBY
delegate :#{attrib}_attributes=, :to => :title_properties, :allow_nil => true
RUBY
end
end
def define_title_properties_associations_accessors
associations = TitleProperties.reflect_on_all_associations
associations.map(&:name).each do |attrib|
class_eval <<-RUBY
def #{attrib}
title_properties.#{attrib}
end
def #{attrib}=(value)
self.title_properties.#{attrib} = value
end
def #{attrib}?
self.title_properties.#{attrib}?
end
RUBY
end
TitleProperties.generated_feature_methods.public_instance_methods(false).grep(/_ids?\Z/).each do |attrib|
class_eval <<-RUBY
def #{attrib}
self.title_properties.#{attrib}
end
def #{attrib}=(value)
self.title_properties.#{attrib} = value
end
RUBY
end
instance_eval <<-RUBY
def reflect_on_all_associations
associations = TitleProperties.reflect_on_all_associations
super + associations
end
def reflect_on_association(assoc)
TitleProperties.reflect_on_association(assoc) || super
end
RUBY
end
def mix_in_title_properties_attributes
class_eval <<-RUBY
def column_for_attribute(attrib)
super || title_properties.column_for_attribute(attrib)
end
def attributes
super.merge(:title_properties => title_properties.attributes)
end
RUBY
end
end
protected
def title_properties_must_be_valid
unless title_properties.valid?
title_properties.errors.each do |attr, message|
errors.add(attr, message)
end
end
end
end
#app/inputs/fake_input.rb
# from https://github.com/plataformatec/simple_form/wiki/Create-an-%22input%22-just-for-displaying-attribute-value
# useful in adding values to a simple_form that it doesn't understand, but I do :)
class FakeInput < SimpleForm::Inputs::StringInput
# This method only create a basic input without reading any value from object
def input
template.text_field_tag(attribute_name, nil, input_html_options)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment