Module: Invoicing::FindSubclasses
- Defined in:
- lib/invoicing/find_subclasses.rb
Overview
Subclass-aware filtering by class methods
Utility module which can be mixed into ActiveRecord::Base
subclasses which use single table inheritance. It enables you to query the database for model objects based on static class properties without having to instantiate more model objects than necessary. Its methods should be used as class methods, so the module should be mixed in using extend
.
For example:
class Product < ActiveRecord::Base
extend Invoicing::FindSubclasses
def self.needs_refrigeration; false; end
end
class Food < Product; end
class Bread < Food; end
class Yoghurt < Food
def self.needs_refrigeration; true; end
end
class GreekYoghurt < Yoghurt; end
class Drink < Product; end
class SoftDrink < Drink; end
class Smoothie < Drink
def self.needs_refrigeration; true; end
end
So we know that all Yoghurt
and all Smoothie
objects need refrigeration (including subclasses of Yoghurt
and Smoothly
, unless they override needs_refrigeration
again), and the others don’t. This fact is defined through a class method and not stored in the database. It needn’t necessarily be constant – you could make needs_refrigeration
return true
or false
depending on the current temperature, for example.
Now assume that in your application you need to query all objects which need refrigeration (and maybe also satisfy some other conditions). Since the database knows nothing about needs_refrigeration
, what you would have to do traditionally is to instantiate all objects and then to filter them yourself, i.e.
Product.find(:all).select{|p| p.class.needs_refrigeration}
However, if only a small proportion of your products needs refrigeration, this requires you to load many more objects than necessary, putting unnecessary load on your application. With the FindSubclasses
module you can let the database do the filtering instead:
Product.find(:all, :conditions => {:needs_refrigeration => true})
You could even define a named scope to do the same thing:
class Product
named_scope :refrigerated_products, :conditions => {:needs_refrigeration => true})
end
Much nicer! The condition looks precisely like a condition on a database table column, even though it actually refers to a class method. Under the hood, this query translates into:
Product.find(:all, :conditions => {:type => ['Yoghurt', 'GreekYoghurt', 'Smoothie']})
And of course you can combine it with normal conditions on database table columns. If there is a table column and a class method with the same name, FindSublasses
remains polite and lets the table column take precedence.
How it works
FindSubclasses
relies on having a list of all subclasses of your single-table-inheritance base class; then, if you specify a condition with a key which has no corresponding database table column, FindSubclasses
will check all subclasses for the return value of a class method with that name, and search for the names of classes which match the condition.
Purists of object-oriented programming will most likely find this appalling, and it’s important to know the limitations. In Ruby, a class can be notified if it subclassed, by defining the Class#inherited
method; we use this to gather together a list of subclasses. Of course, we won’t necessarily know about every class in the world which may subclass our class; in particular, Class#inherited
won’t be called until that subclass is loaded.
If you’re including the Ruby files with the subclass definitions using require
, we will learn about subclasses as soon as they are defined. However, if class loading is delayed until a class is first used (for example, ActiveSupport::Dependencies
does this with model objects in Rails projects), we could run into a situation where we don’t yet know about all subclasses used in a project at the point where we need to process a class method condition. This would cause us to omit some objects we should have found.
To prevent this from happening, this module searches for all types of object currently stored in the table (along the lines of SELECT DISTINCT type FROM table_name
), and makes sure all class names mentioned there are loaded before evaluating a class method condition. Note that this doesn’t necessarily load all subclasses, but at least it loads those which currently have instances stored in the database, so we won’t omit any objects when selecting from the table. There is still room for race conditions to occur, but otherwise it should be fine. If you want to be on the safe side you can ensure all subclasses are loaded when your application initialises – but that’s not completely DRY ;-)
Instance Method Summary collapse
-
#inherited(subclass) ⇒ Object
Ruby callback which is invoked when a subclass is created.
-
#known_subclasses(table = table_name, type_column = inheritance_column) ⇒ Object
Return the list of all known subclasses of this class, if necessary checking the database for classes which have not yet been loaded.
-
#remember_subclass(subclass) ⇒ Object
Add
subclass
to the list of know subclasses of this class. -
#sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) ⇒ Object
Overrides
ActiveRecord::Base.sanitize_sql_hash_for_conditions
since this is the method used to transform a hash of conditions into an SQL query fragment. -
#select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column) ⇒ Object
Returns a list of those classes within
known_subclasses
which match a conditionmethod_name => value
.
Instance Method Details
#inherited(subclass) ⇒ Object
Ruby callback which is invoked when a subclass is created. We use this to build a list of known subclasses.
145 146 147 148 |
# File 'lib/invoicing/find_subclasses.rb', line 145 def inherited(subclass) remember_subclass subclass super end |
#known_subclasses(table = table_name, type_column = inheritance_column) ⇒ Object
Return the list of all known subclasses of this class, if necessary checking the database for classes which have not yet been loaded.
159 160 161 162 |
# File 'lib/invoicing/find_subclasses.rb', line 159 def known_subclasses(table = table_name, type_column = inheritance_column) load_all_subclasses_found_in_database(table, type_column) @known_subclasses ||= [self] end |
#remember_subclass(subclass) ⇒ Object
Add subclass
to the list of know subclasses of this class.
151 152 153 154 155 |
# File 'lib/invoicing/find_subclasses.rb', line 151 def remember_subclass(subclass) @known_subclasses ||= [self] @known_subclasses << subclass unless @known_subclasses.include? subclass self.superclass.remember_subclass(subclass) if self.superclass.respond_to? :remember_subclass end |
#sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) ⇒ Object
Overrides ActiveRecord::Base.sanitize_sql_hash_for_conditions
since this is the method used to transform a hash of conditions into an SQL query fragment. This overriding method searches for class method conditions in the hash and transforms them into a condition on the class name. All further work is delegated back to the superclass method.
Condition formats are very similar to those accepted by ActiveRecord
:
{:my_class_method => 'value'} # known_subclasses.select{|cls| cls.my_class_method == 'value' }
{:my_class_method => [1, 2]} # known_subclasses.select{|cls| [1, 2].include?(cls.my_class_method) }
{:my_class_method => 3..6} # known_subclasses.select{|cls| (3..6).include?(cls.my_class_method) }
{:my_class_method => true} # known_subclasses.select{|cls| cls.my_class_method }
{:my_class_method => false} # known_subclasses.reject{|cls| cls.my_class_method }
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/invoicing/find_subclasses.rb', line 104 def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) new_attrs = {} attrs.each_pair do |attr, value| attr = attr_base = attr.to_s attr_table_name = table_name # Extract table name from qualified attribute names attr_table_name, attr_base = attr.split('.', 2) if attr.include?('.') if columns_hash.include?(attr_base) || ![self.table_name, quoted_table_name].include?(attr_table_name) new_attrs[attr] = value # Condition on a table column, or another table -- pass through unmodified else begin matching_classes = select_matching_subclasses(attr_base, value) new_attrs["#{self.table_name}.#{inheritance_column}"] = matching_classes.map{|cls| cls.name.to_s} rescue NoMethodError new_attrs[attr] = value # If the class method doesn't exist, fall back to passing condition through unmodified end end end super(new_attrs, table_name) end |
#select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column) ⇒ Object
Returns a list of those classes within known_subclasses
which match a condition method_name => value
. May raise NoMethodError
if a class object does not respond to method_name
.
132 133 134 135 136 137 138 139 140 141 |
# File 'lib/invoicing/find_subclasses.rb', line 132 def select_matching_subclasses(method_name, value, table = table_name, type_column = inheritance_column) known_subclasses(table, type_column).select do |cls| returned = cls.send(method_name) (returned == value) or case value when true then !!returned when false then !returned when Array, Range then value.include?(returned) end end end |