diff --git a/lib/tenacity/association.rb b/lib/tenacity/association.rb index f375876..afd84c5 100644 --- a/lib/tenacity/association.rb +++ b/lib/tenacity/association.rb @@ -37,6 +37,9 @@ class Association # Should this association disable foreign key like constraints attr_reader :disable_foreign_key_constraints + + # Filter records based on a defined condition. At this time only activerecord supports this + attr_reader :conditions def initialize(type, name, source, options={}) @type = type @@ -58,6 +61,7 @@ def initialize(type, name, source, options={}) @polymorphic = options[:polymorphic] @as = options[:as] @disable_foreign_key_constraints = options[:disable_foreign_key_constraints] + @conditions = options[:conditions] end # The name of the association diff --git a/lib/tenacity/associations/belongs_to.rb b/lib/tenacity/associations/belongs_to.rb index 62b4bdf..a550608 100644 --- a/lib/tenacity/associations/belongs_to.rb +++ b/lib/tenacity/associations/belongs_to.rb @@ -18,7 +18,7 @@ def _t_cleanup_belongs_to_association(association) def belongs_to_associate(association) associate_id = self.send(association.foreign_key) clazz = association.associate_class(self) - clazz._t_find(associate_id) + clazz._t_find(associate_id,association) end def set_belongs_to_associate(association, associate) diff --git a/lib/tenacity/associations/has_many.rb b/lib/tenacity/associations/has_many.rb index 9f90026..050f5a3 100644 --- a/lib/tenacity/associations/has_many.rb +++ b/lib/tenacity/associations/has_many.rb @@ -27,7 +27,7 @@ def _t_get_associate_ids(association) else foreign_key = association.foreign_key(self.class) associate_id = self.class._t_serialize_ids(self.id, association) - ids = association.associate_class._t_find_all_ids_by_associate(foreign_key, associate_id) + ids = association.associate_class._t_find_all_ids_by_associate(foreign_key, associate_id, association) self.class._t_serialize_ids(ids, association) end end @@ -38,7 +38,7 @@ def has_many_associates(association) ids = _t_get_associate_ids(association) pruned_ids = prune_associate_ids(association, ids) clazz = association.associate_class - clazz._t_find_bulk(pruned_ids) + clazz._t_find_bulk(pruned_ids,association) end def set_has_many_associates(association, associates) @@ -52,7 +52,7 @@ def has_many_associate_ids(association) def set_has_many_associate_ids(association, associate_ids) clazz = association.associate_class - instance_variable_set(_t_ivar_name(association), clazz._t_find_bulk(associate_ids)) + instance_variable_set(_t_ivar_name(association), clazz._t_find_bulk(associate_ids,association)) end def save_without_callback @@ -125,7 +125,7 @@ def save_associate(associate) def get_current_associates(record, association) clazz = association.associate_class property_name = association.foreign_key(record.class) - clazz._t_find_all_by_associate(property_name, _t_serialize(record.id, association)) + clazz._t_find_all_by_associate(property_name, _t_serialize(record.id, association),association) end def destroy_orphaned_associates(association, old_associates, associates) diff --git a/lib/tenacity/associations/has_one.rb b/lib/tenacity/associations/has_one.rb index c2a2f14..6c90666 100644 --- a/lib/tenacity/associations/has_one.rb +++ b/lib/tenacity/associations/has_one.rb @@ -21,7 +21,7 @@ def _t_cleanup_has_one_association(association) def has_one_associate(association) clazz = association.associate_class - clazz._t_find_first_by_associate(association.foreign_key(self.class), _t_serialize(self.id, association)) + clazz._t_find_first_by_associate(association.foreign_key(self.class), _t_serialize(self.id, association),association) end def set_has_one_associate(association, associate) diff --git a/lib/tenacity/orm_ext/activerecord.rb b/lib/tenacity/orm_ext/activerecord.rb index 151572f..3490081 100644 --- a/lib/tenacity/orm_ext/activerecord.rb +++ b/lib/tenacity/orm_ext/activerecord.rb @@ -52,25 +52,45 @@ def _t_id_type @_t_id_type_clazz ||= Kernel.const_get(columns.find{ |x| x.primary }.type.to_s.capitalize) end - def _t_find(id) - find_by_id(_t_serialize(id)) + def _t_merge_association_conditions(base_conditions, association , boolean_operator="AND") + new_conditions = association.conditions unless association == nil + merged_condition = "(#{sanitize_sql(base_conditions)})" + if merged_condition != nil + merged_condition = "#{merged_condition} #{boolean_operator} (#{sanitize_sql(new_conditions)})" if new_conditions != nil + else + merged_condition = new_conditions + end + + merged_condition + end + + def _t_find(id, association = nil ) + if association != nil and association.conditions != nil + find_by_id(_t_serialize(id), association.conditions ) + else + find_by_id(_t_serialize(id) ) + end end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil ) return [] if ids.nil? || ids.empty? - find(:all, :conditions => ["id in (?)", _t_serialize_ids(ids)]) + internal_condition = _t_merge_association_conditions( ["id in (?)", _t_serialize_ids(ids)] , association ) + find(:all, :conditions => internal_condition ) end - def _t_find_first_by_associate(property, id) - find(:first, :conditions => ["#{property} = ?", _t_serialize(id)]) + def _t_find_first_by_associate(property, id, association = nil ) + internal_condition = _t_merge_association_conditions( ["#{property} = ?", _t_serialize(id)] , association ) + find(:first, :conditions => internal_condition ) end - def _t_find_all_by_associate(property, id) - find(:all, :conditions => ["#{property} = ?", _t_serialize(id)]) + def _t_find_all_by_associate(property, id, association = nil ) + internal_condition = _t_merge_association_conditions( ["#{property} = ?", _t_serialize(id)] , association ) + find(:all, :conditions => internal_condition ) end - def _t_find_all_ids_by_associate(property, id) - connection.select_values("SELECT id FROM #{table_name} WHERE #{property} = #{_t_serialize_id_for_sql(id)}") + def _t_find_all_ids_by_associate(property, id, association = nil ) + internal_condition = _t_merge_association_conditions( ["#{property} = ?", _t_serialize(id)] , association ) + connection.select_values("SELECT id FROM #{table_name} WHERE #{internal_condition}") end def _t_initialize_has_one_association(association) diff --git a/lib/tenacity/orm_ext/couchrest.rb b/lib/tenacity/orm_ext/couchrest.rb index 7d906b3..aa19cee 100644 --- a/lib/tenacity/orm_ext/couchrest.rb +++ b/lib/tenacity/orm_ext/couchrest.rb @@ -72,11 +72,11 @@ def _t_id_type String end - def _t_find(id) + def _t_find(id, association = nil) (id.nil? || id.strip == "") ? nil : get(_t_serialize(id)) end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) return [] if ids.nil? || ids.empty? ids = [ids] unless ids.class == Array @@ -88,15 +88,15 @@ def _t_find_bulk(ids) docs.reject { |doc| doc.nil? } end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) self.send("by_#{property}", :key => _t_serialize(id)).first end - def _t_find_all_by_associate(property, id) + def _t_find_all_by_associate(property, id, association = nil) self.send("by_#{property}", :key => _t_serialize(id)) end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) results = self.send("by_#{property}", :key => _t_serialize(id), :include_docs => false) results['rows'].map { |r| r['id'] } end diff --git a/lib/tenacity/orm_ext/datamapper.rb b/lib/tenacity/orm_ext/datamapper.rb index 96c068a..33865bb 100644 --- a/lib/tenacity/orm_ext/datamapper.rb +++ b/lib/tenacity/orm_ext/datamapper.rb @@ -52,24 +52,24 @@ def _t_id_type @_t_id_type_clazz ||= properties.find{ |x| x.key? }.primitive end - def _t_find(id) + def _t_find(id, association = nil) get(_t_serialize(id)) end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) return [] if ids.nil? || ids == [] all(:id => _t_serialize_ids(ids)) end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) first(property => _t_serialize(id)) end - def _t_find_all_by_associate(property, id) + def _t_find_all_by_associate(property, id, association = nil) all(property => _t_serialize(id)) end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) repository.adapter.select("SELECT id from #{storage_names[:default]} WHERE #{property} = #{_t_serialize_id_for_sql(id)}") end diff --git a/lib/tenacity/orm_ext/mongo_mapper.rb b/lib/tenacity/orm_ext/mongo_mapper.rb index 6c22e4d..d95700d 100644 --- a/lib/tenacity/orm_ext/mongo_mapper.rb +++ b/lib/tenacity/orm_ext/mongo_mapper.rb @@ -49,23 +49,23 @@ def _t_id_type String end - def _t_find(id) + def _t_find(id, association = nil) find(_t_serialize(id)) end - def _t_find_bulk(ids=[]) + def _t_find_bulk(ids=[], association = nil) find(_t_serialize_ids(ids)) end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) first(property => _t_serialize(id)) end - def _t_find_all_by_associate(property, id) + def _t_find_all_by_associate(property, id, association = nil) all(property => _t_serialize(id)) end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) results = collection.find({property => _t_serialize(id)}, {:fields => 'id'}).to_a results.map { |r| r['_id'] } end diff --git a/lib/tenacity/orm_ext/mongoid.rb b/lib/tenacity/orm_ext/mongoid.rb index edbcdf7..182888f 100644 --- a/lib/tenacity/orm_ext/mongoid.rb +++ b/lib/tenacity/orm_ext/mongoid.rb @@ -49,28 +49,28 @@ def _t_id_type String end - def _t_find(id) + def _t_find(id, association = nil) (id.nil? || id.to_s.strip == "") ? nil : find(_t_serialize(id)) rescue ::Mongoid::Errors::DocumentNotFound nil end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) docs = find(_t_serialize_ids(ids)) docs.respond_to?(:each) ? docs : [docs] rescue ::Mongoid::Errors::DocumentNotFound [] end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) find(:first, :conditions => { property => _t_serialize(id) }) end - def _t_find_all_by_associate(property, id) + def _t_find_all_by_associate(property, id, association = nil) find(:all, :conditions => { property => _t_serialize(id) }) end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) results = collection.find({property => _t_serialize(id)}, {:fields => 'id'}).to_a results.map { |r| r['_id'] } end diff --git a/lib/tenacity/orm_ext/ripple.rb b/lib/tenacity/orm_ext/ripple.rb index 3233a4a..eb2877f 100644 --- a/lib/tenacity/orm_ext/ripple.rb +++ b/lib/tenacity/orm_ext/ripple.rb @@ -57,17 +57,17 @@ def _t_id_type String end - def _t_find(id) + def _t_find(id, association = nil) find(_t_serialize(id)) end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) objects = find(_t_serialize_ids(ids)) || [] objects = [objects] unless objects.respond_to?(:each) objects.reject(&:nil?) end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) bucket = ::Ripple.client.bucket(_t_bucket_name(property)) if bucket.exist?(id) object = bucket.get(id) @@ -77,11 +77,11 @@ def _t_find_first_by_associate(property, id) end end - def _t_find_all_by_associate(property, id) - find(_t_find_all_ids_by_associate(property, id)) || [] + def _t_find_all_by_associate(property, id, association = nil) + find(_t_find_all_ids_by_associate(property, id, association)) || [] end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) bucket = ::Ripple.client.bucket(_t_bucket_name(property)) if bucket.exist?(id) object = bucket.get(id) diff --git a/lib/tenacity/orm_ext/sequel.rb b/lib/tenacity/orm_ext/sequel.rb index 8704705..e63b390 100644 --- a/lib/tenacity/orm_ext/sequel.rb +++ b/lib/tenacity/orm_ext/sequel.rb @@ -58,24 +58,24 @@ def _t_id_type @_t_id_type_clazz ||= Kernel.const_get(db_schema.values.find{ |x| x[:primary_key] == true }[:type].to_s.capitalize) end - def _t_find(id) + def _t_find(id, association = nil) self[_t_serialize(id)] end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) return [] if ids.nil? || ids.empty? filter(:id => _t_serialize_ids(ids)).to_a end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) first(property.to_sym => _t_serialize(id)) end - def _t_find_all_by_associate(property, id) + def _t_find_all_by_associate(property, id, association = nil) filter(property.to_sym => _t_serialize(id)).to_a end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) results = db["SELECT id FROM #{table_name} WHERE #{property} = #{_t_serialize_id_for_sql(id)}"].all results.map { |r| r[:id] } end diff --git a/lib/tenacity/orm_ext/toystore.rb b/lib/tenacity/orm_ext/toystore.rb index afcbd5b..b62f3b2 100644 --- a/lib/tenacity/orm_ext/toystore.rb +++ b/lib/tenacity/orm_ext/toystore.rb @@ -53,23 +53,23 @@ def _t_id_type String end - def _t_find(id) + def _t_find(id, association = nil) (id.nil? || id.to_s.strip == "") ? nil : get(_t_serialize(id)) end - def _t_find_bulk(ids) + def _t_find_bulk(ids, association = nil) get_multi(_t_serialize_ids(ids)).compact end - def _t_find_first_by_associate(property, id) + def _t_find_first_by_associate(property, id, association = nil) send("first_by_#{property}", id) end - def _t_find_all_by_associate(property, id) - get_multi(_t_find_all_ids_by_associate(property, id)) + def _t_find_all_by_associate(property, id, association = nil) + get_multi(_t_find_all_ids_by_associate(property, id, association)) end - def _t_find_all_ids_by_associate(property, id) + def _t_find_all_ids_by_associate(property, id, association = nil) get_index(property.to_sym, id) end diff --git a/test/association_features/has_many_test.rb b/test/association_features/has_many_test.rb index d7577c4..0db0658 100644 --- a/test/association_features/has_many_test.rb +++ b/test/association_features/has_many_test.rb @@ -1,6 +1,36 @@ require 'test_helper' class HasManyTest < Test::Unit::TestCase + + context "A class with a has_many conditional association to another class" do + setup do + setup_fixtures + setup_couchdb_fixtures + + @car = ActiveRecordCar.create + @driver_seet = ActiveRecordSeet.create(:back => false, :is_driver => true ) + @front_seets = [@driver_seet, ActiveRecordSeet.create(:back => false)] + @back_seets = [ActiveRecordSeet.create(:back => true), ActiveRecordSeet.create(:back => true)] + + @car.active_record_front_seets = @front_seets + @car.active_record_back_seets = @back_seets + @car.save + end + + should "memoize the conditional association" do + assert_equal @driver_seet, @car.active_record_driver_seet(true) + assert_equal @front_seets, @car.active_record_front_seets + assert_equal @back_seets, @car.active_record_back_seets + + other_seets = [ActiveRecordSeet.create(:back => false), ActiveRecordSeet.create(:back => false)] + assert_equal @front_seets, ActiveRecordCar.find(@car.id).active_record_front_seets + ActiveRecordCar.find(@car.id).update_attribute(:active_record_front_seets, other_seets) + assert_equal other_seets, ActiveRecordCar.find(@car.id).active_record_front_seets + + assert_equal @front_seets, @car.active_record_front_seets + assert_equal other_seets, @car.active_record_front_seets(true) + end + end context "A class with a has_many association to another class" do setup do diff --git a/test/fixtures/active_record_car.rb b/test/fixtures/active_record_car.rb index 8089556..78cbf58 100644 --- a/test/fixtures/active_record_car.rb +++ b/test/fixtures/active_record_car.rb @@ -7,5 +7,8 @@ class ActiveRecordCar < ActiveRecord::Base t_has_one :couch_rest_windshield, :foreign_key => :car_id t_has_one :active_record_engine, :foreign_key => 'car_id', :dependent => :nullify + t_has_many :active_record_front_seets , :class_name => "ActiveRecordSeet", :foreign_key => 'car_id' ,:conditions => ["back = ?",false] + t_has_many :active_record_back_seets , :class_name => "ActiveRecordSeet", :foreign_key => 'car_id' ,:conditions => {:back => true} + t_has_one :active_record_driver_seet , :class_name => "ActiveRecordSeet", :foreign_key => 'car_id' ,:conditions => "is_driver = 't' AND back = 'f'" t_has_many :couch_rest_doors, :foreign_key => 'automobile_id', :dependent => :delete_all end diff --git a/test/fixtures/active_record_seat.rb b/test/fixtures/active_record_seat.rb new file mode 100644 index 0000000..850f59f --- /dev/null +++ b/test/fixtures/active_record_seat.rb @@ -0,0 +1,5 @@ +class ActiveRecordSeat < ActiveRecord::Base + include Tenacity + + t_belongs_to :active_record_car, :foreign_key => 'car_id', :polymorphic => false +end diff --git a/test/fixtures/active_record_seet.rb b/test/fixtures/active_record_seet.rb new file mode 100644 index 0000000..4a27032 --- /dev/null +++ b/test/fixtures/active_record_seet.rb @@ -0,0 +1,5 @@ +class ActiveRecordSeet < ActiveRecord::Base + include Tenacity + + t_belongs_to :active_record_car, :foreign_key => 'car_id', :polymorphic => false +end diff --git a/test/helpers/active_record_test_helper.rb b/test/helpers/active_record_test_helper.rb index 325848b..3f26dec 100644 --- a/test/helpers/active_record_test_helper.rb +++ b/test/helpers/active_record_test_helper.rb @@ -86,4 +86,9 @@ t.string :toystore_has_many_target_testable_type end + create_table :active_record_seets, :force => true do |t| + t.boolean :back + t.boolean :is_driver + t.integer :car_id + end end