Ruby on Rails
HowtoMakeAbstractModel

From the beginning, I resisted the idea of inherited classes being used for single-table inheritance, because it broke my hope for an abstract model, similar to the abstract controller you can now use.

The idea, is you make a parent class ‘AbstractModel’ that inherits from ActiveRecord::Base, then your models inherit from AbstractModel. It is a nice place to put extra model functionality you want to add.

The basic AbstractModel
Create /app/models/abstract_model.rb

require 'active_record'
require 'yaml'
class AbstractModel < ActiveRecord::Base
  # add anything you want here

  # these hacks are necessary to fix the default assumption for single-table inheritance    
  def self.descents_from_active_record? # :nodoc:
    #superclass  ActiveRecord::Base
    superclass  AbstractModel
  end

    

def self.class_name_of_active_record_descendant(klass) if klass.superclass == AbstractModel #puts “returning class name as ’#{klass.name}’” return 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
end

My modifications, in case you want to see what is possible, or want to use what I find handy, are here:

  def self.init() YAML::load(File.open(File.dirname(__FILE__) + "/../../config/database.yml")) end
  def init() self.init end # can I do this?
  def sql(statement) <a href="http://wiki.rubyonrails.org/rails/pages/ActiveRecord" class="existingWikiWord">ActiveRecord</a>::Base.connection.execute(statement) end

#each of my models has a REF constant defined:
#REF = %w( * ).map {|a| "articles.#{a}"}.join ', '
# this lets me do things like:

#instead of Article.find(1), if you want it joined with the user table, use Article.ufind(1).
  def self.ufind(item_id, args = {})
    args['add'] = 'user'
    args['limit'] = 1
    return find_by_sql( "SELECT #{table_name}.*,#{User::REF} FROM #{table_name} left join users ON users.id = #{table_name}.user_id " + 
      " WHERE #{table_name}.id = #{item_id}" )
  end

  def self.list_with_user(limit = 20, page = 0, order = "#{table_name}.id DESC", where = '', us = nil)
    where = " WHERE #{where} " if where && !where.length.zero?
    us ||= self.const_get 'REF'

    limit ||= 20; page ||= 0; order ||= "#{table_name}.id DESC" # in case nil is passed in.
    ##{self.const_get 'REF'}
    return find_by_sql( "SELECT #{us},#{User::REF} FROM #{table_name} left join users ON users.id = #{table_name}.user_id " + 
      " #{where} ORDER BY #{order} LIMIT #{page*limit}, #{limit}" )
  end

  def self.sql_join(args)
    b = args[:base].downcase.gsub(/s$/,'') || table_name.downcase.gsub(/s$/,'')
    a = args[:add].downcase.gsub(/s$/,'')

    args[:limit] ||= self.const_get 'LIM'
    args[:id] = "#{a}_id" unless args[:id]
    args[:limit] = "#{args[:page]*args[:limit]}, #{args[:limit]}" if args[:page]

    res = "SELECT \#{#{b.capitalize}::REF},\#{#{a.capitalize}::REF} FROM #{b}s left join #{a}s ON #{a}s.id = #{b}s.#{a}_id " 
    res << "WHERE #{args[:where]} " if args[:where]
    res << "ORDER BY #{args[:order_by]} " if args[:order]
    res << "GROUP BY #{args[:group_by]} " if args[:group]
    res << "LIMIT #{args[:limit]} " if args[:limit]
    $stderr.puts "made sql_list_with: #{res}" 
    res
  end

  #like has_many, only uses the REF and LIM constants that must be defined.  Also allows :add => 'table_to_join_with'
  def self.has(collection_id, options = {})
    validate_options([ :foreign_key, :class_name, :dependent, :conditions, :order, :what, :limit, :finder_sql, :add], options.keys)
    name = collection_id.to_s
    cl = name.gsub(/s$/,'').capitalize
    tbl = table_name.gsub(/s$/,'')

    #forward immediately to regular one if we can't find the REF constant in our collection package?
    #return has_many(collection_id, options) unless Object.const_get( cl ).const_get 'REF'
    options[:order] ||= 'id DESC'
    options[:limit] ||= '#{LIM}'
    options[:what] ||= "\#{#{cl}::REF}" 

    #c = Object.const_get( cl )

    #allow 'add' 
    options[:finder_sql] ||= sql_join(:base => collection_id.to_s, :add => options[:add], :where => "#{tbl}_id = \#{id}") if options[:add]

    options[:finder_sql] ||= "SELECT #{options[:what]} FROM #{name} WHERE #{tbl}_id = \#{id} " + 
      "ORDER BY #{options[:order]} LIMIT #{options[:limit]}"# unless self.const_get 'LIM' # if c and c.const_get 'REF'

    #$stderr.puts "finder_sql is #{options[:finder_sql]}" 
    options.delete :order
    options.delete :what
    options.delete :limit
    options.delete :add
    has_many(collection_id, options)
  end


Instead of making a subclass for this, you can take advantage of Ruby’s dynamicness and just add the methods to ActiveRecord::Base:

class ActiveRecord::Base

  def method_i_want_to_add(args)
    puts "This method is available to all my models." 
  end

end

It’s a little less hackish.


category:Howto