# Overrides ActiveRecord::Base to provide support for table_sql. This allows # the table name in a query to be replaced with a subquery, essentially # simulating a view within ActiveRecord. # # Currently, this has only been tested on MySQL 4.1 and Ruby on Rails 0.13.1. # # This defines two new class methods set_table_sql and set_table_sql_columns. # Both must be used to make a BaseRecord subclass use table_sql. # Usually, set_primary_key will have to be called as well (if the primary key # of the view is not called id). For example, a ParentDetail class could be # defined as: # # belongs_to :parent # set_table_sql(<<-TABLE_SQL # SELECT # parents.id parent_id, # SUM(children.attribute) attrib_sum # FROM # parents # INNER JOIN children ON parents.id = children.parent_id # GROUP BY parents.id # TABLE_SQL # ) # set_table_sql_columns([ # ActiveRecord::ConnectionAdapters::Column.new('parent_id', nil, 'bigint'), # ActiveRecord::ConnectionAdapters::Column.new('attrib_sum', nil, 'bigint')]) # set_primary_key('parent_id') # # Parent, which should also inherit from BaseRecord, could then be defined as: # # has_many :children # has_one :parent_detail # # You can then perform eager loading finds like the following: # # Parent.find(:all, :include => [:parent_detail, other includes...]) # # This find will produce SQL like the following: # # SELECT # parents.id, # other parents fields..., # parent_details.parent_id, # parent_details.attrib_sum, # FROM # parents # LEFT OUTER JOIN () parent_details ON parent_details.parent_id = parents.id # # To upgrade to future versions of ActiveRecord (this was based on RoR 0.13.1, # ActiveRecord 1.11.1): # * Check for any changes to the overridden methods and update as necessary: # * columns # * construct_finder_sql # * construct_finder_sql_with_included_associations # * association_join # * class_name_of_active_record_descendant # * attributes_from_column_definition # * Check for any new methods that access connection.columns directly and # override to call table_sql_columns instead. class BaseRecord < ActiveRecord::Base class << self # The SQL to use in place of the table name (nil by default => use table_name). def table_sql nil end # Set the table SQL. def set_table_sql( value=nil, &block ) define_attr_method :table_sql, value, &block end alias :table_sql= :set_table_sql # Columns for the table, by default retreived from the database connection. def table_sql_columns connection.columns(table_name, "#{name} Columns") end # Set the columns to use for table SQL (required if using association eager loading). def set_table_sql_columns(value=nil, &block) define_val_attr_method :table_sql_columns, value, &block end alias :table_sql_columns= :set_table_sql_columns # The name of the table to use in SQL queries - either just hte table name # or the table_sql in parentheses followed by the table_name used to alias # the sub query. def table_name_for_sql table_sql.nil? ? table_name : "(#{table_sql}) #{table_name}" end # Overridden from ActiveRecord::Base to use table_sql_columns rather than # a direct call to connection. def columns @columns ||= table_sql_columns end private # Similar to define_attr_method in ActiveRecord::Base, but doesn't # convert result to string def define_val_attr_method(name, value=nil, &block) sing = class << self; self; end block = proc { value } if value sing.send( :alias_method, "original_#{name}", name ) sing.send( :define_method, name, &block ) end # Overridden from ActiveRecord::Base to build SQL to select from a single # table. The only change here is to use #{table_name_for_sql} instead of # #{table_name} def construct_finder_sql(options) sql = "SELECT * FROM #{table_name_for_sql} " sql << " #{options[:joins]} " if options[:joins] add_conditions!(sql, options[:conditions]) sql << "ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options) sql end # Overridden from associations to build SQL to select from a join. The # only change here is to use #{table_name_for_sql} # instead of #{table_name}. def construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections) sql = "SELECT #{column_aliases(schema_abbreviations)} FROM #{table_name_for_sql} " sql << reflections.collect { |reflection| association_join(reflection) }.to_s sql << "#{options[:joins]} " if options[:joins] add_conditions!(sql, options[:conditions]) add_sti_conditions!(sql, reflections) sql << "ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options) if using_limitable_reflections?(reflections) return sanitize_sql(sql) end # Overridden from associations to build SQL to select from a join. The # only change here is to use #{reflection.klass.table_name_for_sql} # instead of #{reflection.klass.table_name}. def association_join(reflection) case reflection.macro when :has_and_belongs_to_many " LEFT OUTER JOIN #{reflection.options[:join_table]} ON " + "#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " + "#{table_name}.#{primary_key} " + " LEFT OUTER JOIN #{reflection.klass.table_name_for_sql} ON " + "#{reflection.options[:join_table]}.#{reflection.options[:association_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " + "#{reflection.klass.table_name}.#{reflection.klass.primary_key} " when :has_many, :has_one " LEFT OUTER JOIN #{reflection.klass.table_name_for_sql} ON " + "#{reflection.klass.table_name}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " + "#{table_name}.#{primary_key} " when :belongs_to " LEFT OUTER JOIN #{reflection.klass.table_name_for_sql} ON " + "#{reflection.klass.table_name}.#{reflection.klass.primary_key} = " + "#{table_name}.#{reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key} " else "" end end protected # Overridden from ActiveRecord::Base this to look for subclasses of # BaseRecord rather than ActiveRecord::Base when determining the class name. def class_name_of_active_record_descendant(klass) if klass.superclass == BaseRecord klass.name elsif klass.superclass.nil? raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" else class_name_of_active_record_descendant(klass.superclass) end end public # Overridden to call table_sql_columns rather than connection.columns. def attributes_from_column_definition table_sql_columns.inject({}) do |attributes, column| attributes[column.name] = column.default unless column.name == self.class.primary_key attributes end end end end