# 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