Class: ActiveRecord::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/class_table_inheritance.rb

Overview

This library makes an ActiveRecord subclass model use one table per subclass level. Instances are instances of the _super class_, with a proxy instance handling the subclass attributes and method calls.

Class Method Summary collapse

Class Method Details

.class_table_inheritance(options = {}) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/class_table_inheritance.rb', line 28

def class_table_inheritance(options = {})
  table_name = options[:subclass_table] || name.demodulize.tableize
  primary_key_name = options[:subclass_foreign_key] || "#{superclass.name.demodulize.underscore}_id"
  
  has_extra_columns_from(
                         :table => table_name, :foreign_key => primary_key_name,
                         :proxy_class_name => 'ExtraColumns',
  :proxy_class_belongs_to => self.name,
  :delegate_new_associations => true,
  :save_before_superclass_callbacks => true
  )
end

.find_with_default_eager_loading(*params) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/class_table_inheritance.rb', line 11

def find_with_default_eager_loading(*params)
  if default_eager_loading
    if params.last.is_a?(Hash)
      opts = params.last
    else
      opts = {}
      params.push(opts)
    end
    opts[:include] ||= []
    opts[:include] = [opts[:include]] unless opts[:include].is_a?(Array)
    opts[:include] += default_eager_loading
  end
  find_without_default_eager_loading(*params)
end

.has_extra_columns_from(options, &block) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/class_table_inheritance.rb', line 41

def has_extra_columns_from(options, &block)
  table_name = options[:table]
  primary_key_name = options[:foreign_key] || "#{name.demodulize.underscore}_id"
  
  proxy_class_name = options[:proxy_class_name] || "ExtraColumnsFor#{table_name.camelize}"
  proxy_class_namespace = (options[:proxy_class_namespace] || name).constantize
  proxy_symbol = "extra_columns_for_#{table_name}".to_sym
  
  proxy_class = proxy_class_namespace.const_get(proxy_class_name) rescue nil
  unless proxy_class
    proxy_class = proxy_class_namespace.const_set(proxy_class_name, Class.new(ActiveRecord::Base))    
    proxy_class.class_eval do
      set_table_name table_name
      set_primary_key primary_key_name
      def self.reloadable?; false; end
    end
    proxy_class.class_eval(&block) if block_given?
  end
  
  has_one_without_cti proxy_symbol, :class_name => proxy_class.name, :foreign_key => primary_key_name, :dependent => :destroy
  validates_associated proxy_symbol
  
  if options[:save_before_superclass_callbacks] || true
    # We need the after_save filter for this association to run /before/ any other after_save's already registered on a superclass,
    # and before any after_creates or after_updates. This calls for some hackery:
    proxy_save_callback = @inheritable_attributes[:after_save].pop
    @inheritable_attributes[:after_create] ||= []
    @inheritable_attributes[:after_create].unshift(proxy_save_callback)
    @inheritable_attributes[:after_update] ||= []
    @inheritable_attributes[:after_update].unshift(proxy_save_callback)
  end
  
  # adds this to the class-inheritable array of default eager loads:
  self.default_eager_loading = [proxy_symbol] unless options[:default_eager_loading] == false
  
  class_eval <<-EOV
    alias_method :#{proxy_symbol}_old, :#{proxy_symbol}
    def #{proxy_symbol}
      #{proxy_symbol}_old or self.#{proxy_symbol} = #{proxy_class.name}.new
    end
    def save(*args)
      self.#{proxy_symbol} ||= #{proxy_class.name}.new
      super
    end
    # this doesn't happen automatically on update, so we'll make it:
    after_update {|record| record.#{proxy_symbol}.save }
    
    alias_method :write_attribute_super, :write_attribute
    def write_attribute(attr_name, attr_value)
      if self.class.columns.find {|c| c.name == attr_name}
        write_attribute_super(attr_name, attr_value)
      else
        self.#{proxy_symbol}.write_attribute(attr_name, attr_value)
      end
    end
      
    alias_method :old_attributes, :attributes
    def attributes
      all_attributes = old_attributes.update(self.#{proxy_symbol}.attributes)
      all_attributes.delete(#{proxy_class_name}.primary_key) if self.class.primary_key != #{proxy_class_name}.primary_key
      all_attributes
    end

    class << self
      alias_method :method_missing_super, :method_missing
      def method_missing(method_symbol, *parameters)
        if method_symbol.to_s =~ /^find_by_(.+)/
          attributes = $1.split('_and_')
          super_attributes = attributes & columns.map {|c| c.name}
          sub_attributes = attributes - super_attributes
          if super_attributes.size > 0
            # TODO (uwe): Only use super attributes first
            # then filter by sub_attributes
            method_missing_super(method_symbol, *parameters)
          else
            result = #{proxy_class_name}.method_missing(method_symbol, parameters)
            if result
              if result.is_a? Array
                result.map {|entry| entry.base}
              else
                result = result.base
              end
            else
              result
            end
          end
        else
          method_missing_super(method_symbol, *parameters)
        end
      end
# TODO (uwe): validates_uniqueness_of should only be delegated for colimns not in super class???
      delegate :validates_uniqueness_of, {:to => #{proxy_class_name}}

      delegate :foreign_keys, {:to => #{proxy_class_name}}
    end
  EOV
  
  delegate_methods = proxy_class.column_names
  delegate_methods += proxy_class.column_names.map {|name| "#{name}=".to_sym }
  delegate_methods += proxy_class.column_names.map {|name| "#{name}?".to_sym }
  proxy_class.reflect_on_all_associations.each do |reflection|
    delegate_methods += case reflection.macro
    when :has_many, :has_and_belongs_to_many
      [reflection.name, "#{reflection.name}=".to_sym, "#{reflection.name.to_s.singularize}_ids=".to_sym]
    when :has_one
      [reflection.name, "#{reflection.name}=".to_sym, "build_#{reflection.name}".to_sym, "create_#{reflection.name}".to_sym]
    when :belongs_to
      [reflection.name, "#{reflection.name}=".to_sym, "#{reflection.name}?".to_sym, "build_#{reflection.name}".to_sym, "create_#{reflection.name}".to_sym]
    end
  end
  delegate(*(delegate_methods << {:to => proxy_symbol}))
  
  proxy_class.belongs_to(:base, :class_name => options[:proxy_class_belongs_to], :foreign_key => primary_key_name) if options[:proxy_class_belongs_to]
  
  if options[:delegate_new_associations]
    class_eval <<-EOV
      # associations on this subclass get added to the proxy class, and then the relevant methods delegated to the proxy object
      def self.belongs_to(name, *params)
        ExtraColumns.belongs_to(name, *params)
        delegate name, "\#{name}=".to_sym, "\#{name}?".to_sym, "build_\#{name}".to_sym, "create_\#{name}".to_sym, :to => :#{proxy_symbol}
      end

      def self.has_one(name, *params)
        ExtraColumns.has_one(name, *params)
        delegate name, "\#{name}=".to_sym, "build_\#{name}".to_sym, "create_\#{name}".to_sym, :to => :#{proxy_symbol}
      end

      def self.has_many(name, *params)
        ExtraColumns.has_many(name, *params)
        delegate name, "\#{name}=".to_sym, "\#{name.to_s.singularize}_ids=".to_sym, :to => :#{proxy_symbol}
      end

      def self.has_and_belongs_to_many(name, *params)
        ExtraColumns.has_and_belongs_to_many(name, *params)
        delegate name, "\#{name}=".to_sym, "\#{name.to_s.singularize}_ids=".to_sym, :to => :#{proxy_symbol}
      end
    EOV
  end
end