diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
3 files changed, 338 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb new file mode 100644 index 0000000000..a60b9ddab5 --- /dev/null +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -0,0 +1,129 @@ +module ActiveRecord + module Associations + class AssociationCollection #:nodoc: + alias_method :proxy_respond_to?, :respond_to? + instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ } + + def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + @owner = owner + @options = options + @association_name = association_name + @association_class = eval(association_class_name) + @association_class_primary_key_name = association_class_primary_key_name + end + + def method_missing(symbol, *args, &block) + load_collection + @collection.send(symbol, *args, &block) + end + + def to_ary + load_collection + @collection.to_ary + end + + def respond_to?(symbol) + proxy_respond_to?(symbol) || [].respond_to?(symbol) + end + + def loaded? + !@collection.nil? + end + + def reload + @collection = nil + end + + # Add +records+ to this association. Returns +self+ so method calls may be chained. + # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + def <<(*records) + flatten_deeper(records).each do |record| + raise_on_type_mismatch(record) + insert_record(record) + @collection << record if loaded? + end + self + end + + alias_method :push, :<< + alias_method :concat, :<< + + # Remove +records+ from this association. Does not destroy +records+. + def delete(*records) + records = flatten_deeper(records) + records.each { |record| raise_on_type_mismatch(record) } + delete_records(records) + records.each { |record| @collection.delete(record) } if loaded? + end + + def destroy_all + each { |record| record.destroy } + @collection = [] + end + + def size + if loaded? then @collection.size else count_records end + end + + def empty? + size == 0 + end + + def uniq(collection = self) + collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records } + end + + alias_method :length, :size + + protected + def loaded? + not @collection.nil? + end + + def quoted_record_ids(records) + records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',') + end + + def interpolate_sql_options!(options, *keys) + keys.each { |key| options[key] &&= interpolate_sql(options[key]) } + end + + def interpolate_sql(sql, record = nil) + @owner.send(:interpolate_sql, sql, record) + end + + private + def load_collection + begin + @collection = find_all_records unless loaded? + rescue ActiveRecord::RecordNotFound + @collection = [] + end + end + + def raise_on_type_mismatch(record) + raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class) + end + + + def load_collection_to_array + return unless @collection_array.nil? + begin + @collection_array = find_all_records + rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound + @collection_array = [] + end + end + + def duplicated_records_array(records) + records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection) + records.dup + end + + # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems. + def flatten_deeper(array) + array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb new file mode 100644 index 0000000000..946f238f21 --- /dev/null +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -0,0 +1,107 @@ +module ActiveRecord + module Associations + class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options) + super(owner, association_name, association_class_name, association_class_primary_key_name, options) + + @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id" + association_table_name = options[:table_name] || @association_class.table_name(association_class_name) + @join_table = join_table + @order = options[:order] || "t.#{@owner.class.primary_key}" + + interpolate_sql_options!(options, :finder_sql, :delete_sql) + @finder_sql = options[:finder_sql] || + "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " + + "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " + + "j.#{association_class_primary_key_name} = '#{@owner.id}' " + + (options[:conditions] ? " AND " + options[:conditions] : "") + " " + + "ORDER BY #{@order}" + end + + # Removes all records from this association. Returns +self+ so method calls may be chained. + def clear + return self if size == 0 # forces load_collection if hasn't happened already + + if sql = @options[:delete_sql] + each { |record| @owner.connection.execute(sql) } + elsif @options[:conditions] + sql = + "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " + + "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})" + @owner.connection.execute(sql) + else + sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'" + @owner.connection.execute(sql) + end + + @collection = [] + self + end + + def find(association_id = nil, &block) + if block_given? || @options[:finder_sql] + load_collection + @collection.find(&block) + else + if loaded? + find_all { |record| record.id == association_id.to_i }.first + else + find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first + end + end + end + + def push_with_attributes(record, join_attributes = {}) + raise_on_type_mismatch(record) + insert_record_with_join_attributes(record, join_attributes) + join_attributes.each { |key, value| record.send(:write_attribute, key, value) } + @collection << record if loaded? + self + end + + alias :concat_with_attributes :push_with_attributes + + def size + @options[:uniq] ? count_records : super + end + + protected + def find_all_records(sql = @finder_sql) + records = @association_class.find_by_sql(sql) + @options[:uniq] ? uniq(records) : records + end + + def count_records + load_collection + @collection.size + end + + def insert_record(record) + if @options[:insert_sql] + @owner.connection.execute(interpolate_sql(@options[:insert_sql], record)) + else + sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')" + @owner.connection.execute(sql) + end + end + + def insert_record_with_join_attributes(record, join_attributes) + attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes) + sql = + "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " + + "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})" + @owner.connection.execute(sql) + end + + def delete_records(records) + if sql = @options[:delete_sql] + records.each { |record| @owner.connection.execute(sql) } + else + ids = quoted_record_ids(records) + sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})" + @owner.connection.execute(sql) + end + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb new file mode 100644 index 0000000000..947862ad37 --- /dev/null +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -0,0 +1,102 @@ +module ActiveRecord + module Associations + class HasManyAssociation < AssociationCollection #:nodoc: + def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options) + super(owner, association_name, association_class_name, association_class_primary_key_name, options) + @conditions = @association_class.send(:sanitize_conditions, options[:conditions]) + + if options[:finder_sql] + @finder_sql = interpolate_sql(options[:finder_sql]) + @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM") + else + @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}" + @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}" + end + end + + def create(attributes = {}) + # Can't use Base.create since the foreign key may be a protected attribute. + record = build(attributes) + record.save + @collection << record if loaded? + record + end + + def build(attributes = {}) + record = @association_class.new(attributes) + record[@association_class_primary_key_name] = @owner.id + record + end + + def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block) + if block_given? || @options[:finder_sql] + load_collection + @collection.find_all(&block) + else + @association_class.find_all( + "#{@association_class_primary_key_name} = '#{@owner.id}' " + + "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}", + orderings, + limit, + joins + ) + end + end + + def find(association_id = nil, &block) + if block_given? || @options[:finder_sql] + load_collection + @collection.find(&block) + else + @association_class.find_on_conditions(association_id, + "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}" + ) + end + end + + # Removes all records from this association. Returns +self+ so + # method calls may be chained. + def clear + @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'") + @collection = [] + self + end + + protected + def find_all_records + if @options[:finder_sql] + @association_class.find_by_sql(@finder_sql) + else + @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil) + end + end + + def count_records + if has_cached_counter? + @owner.send(:read_attribute, cached_counter_attribute_name) + elsif @options[:finder_sql] + @association_class.count_by_sql(@counter_sql) + else + @association_class.count(@counter_sql) + end + end + + def has_cached_counter? + @owner.attribute_present?(cached_counter_attribute_name) + end + + def cached_counter_attribute_name + "#{@association_name}_count" + end + + def insert_record(record) + record.update_attribute(@association_class_primary_key_name, @owner.id) + end + + def delete_records(records) + ids = quoted_record_ids(records) + @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})") + end + end + end +end |