Class: LowCardTables::HasLowCardTable::LowCardDynamicMethodManager
- Inherits:
-
Object
- Object
- LowCardTables::HasLowCardTable::LowCardDynamicMethodManager
- Defined in:
- lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb
Overview
This object is responsible for maintaining the set of methods that get automatically delegated when you declare has_low_card_table
on an ::ActiveRecord model – it both maintains the set of methods defined on the _low_card_dynamic_methods_module for that class, and directs the calls in the right place at runtime.
Secondarily, it also is responsible for transforming query specifications – #scope_from_query takes the set of constraints you passed, as a Hash, to ::ActiveRecord::Relation#where, and transforms them using low-card information. So:
{ :deleted => true, :deceased => false }
might become
{ :user_status_id => [ 1, 3, 9, 17 ] }
While it might seem odd for that functionality to live in this class, it actually makes sense; this is the class that knows what method names in the low-card class those keys map to, after all.
Instance Method Summary collapse
-
#initialize(model_class) ⇒ LowCardDynamicMethodManager
constructor
A new instance of LowCardDynamicMethodManager.
-
#run_low_card_method(object, method_name, args) ⇒ Object
Given an instance of the model class we’re maintaining methods for, the name of a method to invoke, and arguments passed to that method, runs the correct method.
-
#scope_from_query(base_scope, query_hash) ⇒ Object
Given a base ::ActiveRecord::Relation scope (which can of course just be a model class itself), and a set of query constraints as passed into ::ActiveRecord::Relation#where (which must be a Hash – for the other forms of #where, our override of ::ActiveRecord::Relation#where doesn’t call this method but just passes through to the underlying method), returns a new scope that is the result of applying those constraints correctly to the
base_scope
. -
#sync_methods! ⇒ Object
This method is responsible for doing two things:.
Constructor Details
#initialize(model_class) ⇒ LowCardDynamicMethodManager
Returns a new instance of LowCardDynamicMethodManager.
20 21 22 23 |
# File 'lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb', line 20 def initialize(model_class) @model_class = model_class @method_delegation_map = { } end |
Instance Method Details
#run_low_card_method(object, method_name, args) ⇒ Object
Given an instance of the model class we’re maintaining methods for, the name of a method to invoke, and arguments passed to that method, runs the correct method. This is therefore a dispatcher – rather than attempt to define the methods on the _low_card_dynamic_methods_module at all times to directly call the right low-card object, we simply have them all call through here, instead, and do the dispatch at runtime. This simplifies the nature of the dynamic methods considerably.
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb', line 30 def run_low_card_method(object, method_name, args) ensure_correct_class!(object) method_data = @method_delegation_map[method_name.to_s] unless method_data raise NameError, "Whoa -- we're trying to call a delegated low-card method #{method_name.inspect} on #{object}, of class #{object.class}, but somehow the LowCardDynamicMethodManager has no knowledge of that method?!? We know about: #{@method_delegation_map.keys.sort.inspect}" end (association, association_method_name) = method_data if association_method_name == :_low_card_object # e.g., my_user.status object._low_card_objects_manager.object_for(association) elsif association_method_name == :_low_card_foreign_key # e.g., my_user.user_status_id object._low_card_objects_manager.foreign_key_for(association) elsif association_method_name == :_low_card_foreign_key= # e.g., my_user.user_status_id = object._low_card_objects_manager.set_foreign_key_for(association, *args) else # e.g., my_user.deleted = low_card_object = object.send(association.association_name) low_card_object.send(association_method_name, *args) end end |
#scope_from_query(base_scope, query_hash) ⇒ Object
Given a base ::ActiveRecord::Relation scope (which can of course just be a model class itself), and a set of query constraints as passed into ::ActiveRecord::Relation#where (which must be a Hash – for the other forms of #where, our override of ::ActiveRecord::Relation#where doesn’t call this method but just passes through to the underlying method), returns a new scope that is the result of applying those constraints correctly to the base_scope
.
The constraints in the query_hash need not all be, or even any be, constraints on a low-card table; any non- low-card constraints are simply passed through verbatim. But constraints on a low-card table – whether they’re implicit, like
User.where(:deleted => false)
or explicit, like
User.where(:status => { :deleted => false })
…are transformed into explicit references to the low-card foreign-key column:
User.where(:user_status_id => [ 1, 3, 4, 7, 8, 10 ])
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 |
# File 'lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb', line 75 def scope_from_query(base_scope, query_hash) non_low_card_constraints = { } low_card_association_to_constraint_map = { } # We iterate through the query hash, building up two hashes: # # * non_low_card_constraints is the set of all constraints that have nothing to do with a low-card table; # * low_card_association_to_constraint_map maps low-card association names to a Hash of the constraints applied # to that association; the constraints in the Hash use key names that are the actual low-card column names # (i.e., we translate them from whatever delegated method names were present in the referring class) query_hash.each do |query_key, query_value| low_card_delegation = @method_delegation_map[query_key.to_s] # Does this constraint even mention a low-card column or association name? if low_card_delegation (association, method) = low_card_delegation # e.g., User.where(:status => { ... }) if method == :_low_card_object if (! query_value.kind_of?(Hash)) raise ArgumentError, %{You are trying to constrain on #{@model_class.name}.#{query_key}, which is a low-card association, but the value you passed, #{query_value.inspect}, is not a Hash. Either pass a Hash, or constrain on #{association.foreign_key_column_name} explicitly, and find IDs yourself, using #{association.low_card_class.name}#ids_matching.} end low_card_association_to_constraint_map[association] ||= { } low_card_association_to_constraint_map[association].merge!(query_value) # e.g., User.where(:user_status_id => ...) elsif method == :_low_card_foreign_key non_low_card_constraints[query_key] = query_value # e.g., User.where(:deleted => false) else low_card_association_to_constraint_map[association] ||= { } low_card_association_to_constraint_map[association][method] = query_value end else # e.g., User.where(:name => ...) non_low_card_constraints[query_key] = query_value end end out = base_scope # See the comment in LowCardTables::ActiveRecord::Relation -- this is so that when we call #where, below, # we don't end up creating infinite mutual recursion. +_low_card_direct+ is our 'escape hatch'. out = base_scope.where(non_low_card_constraints.merge(:_low_card_direct => true)) if non_low_card_constraints.size > 0 # This is gross. In ActiveRecord v3, doing something like this: # # Model.where(:x => [ 1, 2, 3 ]).where(:x => [ 3, 4, 5 ]) # # ...results in "... WHERE x IN (3, 4, 5)" -- i.e., it's last-clause wins, and the first one is totally # ignored. While this sucks in general (in my opinion), it's genuinely a problem for our system; we need to # be able to say Model.where(:deleted => false).where(:deceased => false) and only get back non-deleted, alive # users -- and, underneath, both those queries transform to conditions on :user_status_id. # # Our workaround is to instead use text-based queries for these conditions, because: # # Model.where("x IN :ids", :ids => [ 1, 2, 3 ]).where("x IN :ids", :ids => [ 3, 4, 5 ]) # # ...results in "... WHERE x IN (1, 2, 3) AND x IN (3, 4, 5)", which gives us the right value. (ActiveRecord # doesn't ever parse SQL you hand to it, so it has no way of knowing these are conditions on the same column -- # so it keeps both clauses.) # # ActiveRecord 4 does the right thing here (IMHO) and behaves identically whether you pass in a Hash or a text # clause. However, our hack works fine with both versions, so we'll keep it for now. low_card_association_to_constraint_map.each do |association, constraints| ids = association.low_card_class.low_card_ids_matching(constraints) out = out.where("#{association.foreign_key_column_name} IN (:ids)", :ids => ids) end out end |
#sync_methods! ⇒ Object
This method is responsible for doing two things:
-
Most importantly, it sets up @method_delegation_map. This maps the name of every dynamic method that can be invoked on an instance of the model class to the low-card method that it should delegate to. (It calls LowCardTables::HasLowCardTable::LowCardAssociation#class_method_name_to_low_card_method_name_map to figure out what methods should be delegated for each association.) There are a few ‘special’ method names:
:_low_card_object
means ‘return the associated low-card object itself’ (e.g., my_user.status);:_low_card_foreign_key
means ‘return the associated low-card foreign key’ (e.g., my_user.user_status_id);:_low_card_foreign_key=
means ‘set the associated low-card foreign key’. -
Secondly, it makes sure that, for each of these methods, the _low_card_dynamic_methods_module has installed a method that delegates to #run_low_card_method on this object – and that no other methods are installed on that module.
This method implements the ‘last association wins’ policy, by simply going through the asssociations in order of definition and letting them overwrite previous associations’ method names, if they collide.
Rather than trying to dynamically add and remove methods as associations are added, columns are removed, etc., it is far simpler to do what we do here: simply rebuild the map from scratch on each call – and then apply the differences to the _low_card_dynamic_methods_module.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/low_card_tables/has_low_card_table/low_card_dynamic_method_manager.rb', line 169 def sync_methods! currently_delegated_methods = @method_delegation_map.keys @method_delegation_map = { } associations.each do |association| @method_delegation_map[association.association_name.to_s] = [ association, :_low_card_object ] @method_delegation_map[association.foreign_key_column_name.to_s] = [ association, :_low_card_foreign_key ] @method_delegation_map[association.foreign_key_column_name.to_s + "="] = [ association, :_low_card_foreign_key= ] association.class_method_name_to_low_card_method_name_map.each do |desired_name, association_method_name| desired_name = desired_name.to_s @method_delegation_map[desired_name] = [ association, association_method_name ] end end remove_delegated_methods!(currently_delegated_methods - @method_delegation_map.keys) add_delegated_methods!(@method_delegation_map.keys - currently_delegated_methods) end |