X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=vendor%2Frails%2Factiverecord%2Flib%2Factive_record%2Fassociations%2Fassociation_collection.rb;fp=vendor%2Frails%2Factiverecord%2Flib%2Factive_record%2Fassociations%2Fassociation_collection.rb;h=0ff91fbdf822db9962f559d2586a2e869fe60e8c;hb=d115f2e23823271635bad69229a42cd8ac68debe;hp=0000000000000000000000000000000000000000;hpb=37cb670bf3ddde90b214e591f100ed4446469484;p=depot.git diff --git a/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb new file mode 100644 index 0000000..0ff91fb --- /dev/null +++ b/vendor/rails/activerecord/lib/active_record/associations/association_collection.rb @@ -0,0 +1,453 @@ +require 'set' + +module ActiveRecord + module Associations + # AssociationCollection is an abstract class that provides common stuff to + # ease the implementation of association proxies that represent + # collections. See the class hierarchy in AssociationProxy. + # + # You need to be careful with assumptions regarding the target: The proxy + # does not fetch records from the database until it needs them, but new + # ones created with +build+ are added to the target. So, the target may be + # non-empty and still lack children waiting to be read from the database. + # If you look directly to the database you cannot assume that's the entire + # collection because new records may have beed added to the target, etc. + # + # If you need to work on all current children, new and existing records, + # +load_target+ and the +loaded+ flag are your friends. + class AssociationCollection < AssociationProxy #:nodoc: + def initialize(owner, reflection) + super + construct_sql + end + + def find(*args) + options = args.extract_options! + + # If using a custom finder_sql, scan the entire collection. + if @reflection.options[:finder_sql] + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + else + conditions = "#{@finder_sql}" + if sanitized_conditions = sanitize_sql(options[:conditions]) + conditions << " AND (#{sanitized_conditions})" + end + + options[:conditions] = conditions + + if options[:order] && @reflection.options[:order] + options[:order] = "#{options[:order]}, #{@reflection.options[:order]}" + elsif @reflection.options[:order] + options[:order] = @reflection.options[:order] + end + + # Build options specific to association + construct_find_options!(options) + + merge_options_from_reflection!(options) + + # Pass through args exactly as we received them. + args << options + @reflection.klass.find(*args) + end + end + + # Fetches the first one using SQL if possible. + def first(*args) + if fetch_first_or_last_using_find?(args) + find(:first, *args) + else + load_target unless loaded? + @target.first(*args) + end + end + + # Fetches the last one using SQL if possible. + def last(*args) + if fetch_first_or_last_using_find?(args) + find(:last, *args) + else + load_target unless loaded? + @target.last(*args) + end + end + + def to_ary + load_target + @target.to_ary + end + + def reset + reset_target! + @loaded = false + end + + def build(attributes = {}, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| build(attr, &block) } + else + build_record(attributes) do |record| + block.call(record) if block_given? + set_belongs_to_association_for(record) + end + end + 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) + result = true + load_target if @owner.new_record? + + transaction do + flatten_deeper(records).each do |record| + raise_on_type_mismatch(record) + add_record_to_target_with_callbacks(record) do |r| + result &&= insert_record(record) unless @owner.new_record? + end + end + end + + result && self + end + + alias_method :push, :<< + alias_method :concat, :<< + + # Starts a transaction in the association class's database connection. + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.find(:first).books.transaction do + # # same effect as calling Book.transaction + # end + def transaction(*args) + @reflection.klass.transaction(*args) do + yield + end + end + + # Remove all records from this association + def delete_all + load_target + delete(@target) + reset_target! + end + + # Calculate sum using SQL, not Enumerable + def sum(*args) + if block_given? + calculate(:sum, *args) { |*block_args| yield(*block_args) } + else + calculate(:sum, *args) + end + end + + # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will + # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the + # descendant's +construct_sql+ method will have set :counter_sql automatically. + # Otherwise, construct options and pass them with scope to the target class's +count+. + def count(*args) + if @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) + else + column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) + if @reflection.options[:uniq] + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all + options.merge!(:distinct => true) + end + + value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } + + limit = @reflection.options[:limit] + offset = @reflection.options[:offset] + + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value + end + end + end + + + # Removes +records+ from this association calling +before_remove+ and + # +after_remove+ callbacks. + # + # This method is abstract in the sense that +delete_records+ has to be + # provided by descendants. Note this method does not imply the records + # are actually removed from the database, that depends precisely on + # +delete_records+. They are in any case removed from the collection. + def delete(*records) + records = flatten_deeper(records) + records.each { |record| raise_on_type_mismatch(record) } + + transaction do + records.each { |record| callback(:before_remove, record) } + + old_records = records.reject {|r| r.new_record? } + delete_records(old_records) if old_records.any? + + records.each do |record| + @target.delete(record) + callback(:after_remove, record) + end + end + end + + # Removes all records from this association. Returns +self+ so method calls may be chained. + def clear + return self if length.zero? # forces load_target if it hasn't happened already + + if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy + destroy_all + else + delete_all + end + + self + end + + def destroy_all + transaction do + each { |record| record.destroy } + end + + reset_target! + end + + def create(attrs = {}) + if attrs.is_a?(Array) + attrs.collect { |attr| create(attr) } + else + create_record(attrs) do |record| + yield(record) if block_given? + record.save + end + end + end + + def create!(attrs = {}) + create_record(attrs) do |record| + yield(record) if block_given? + record.save! + end + end + + # Returns the size of the collection by executing a SELECT COUNT(*) + # query if the collection hasn't been loaded, and calling + # collection.size if it has. + # + # If the collection has been already loaded +size+ and +length+ are + # equivalent. If not and you are going to need the records anyway + # +length+ will take one less query. Otherwise +size+ is more efficient. + # + # This method is abstract in the sense that it relies on + # +count_records+, which is a method descendants have to provide. + def size + if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) + @target.size + elsif !loaded? && @reflection.options[:group] + load_target.size + elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) + unsaved_records = @target.select { |r| r.new_record? } + unsaved_records.size + count_records + else + count_records + end + end + + # Returns the size of the collection calling +size+ on the target. + # + # If the collection has been already loaded +length+ and +size+ are + # equivalent. If not and you are going to need the records anyway this + # method will take one less query. Otherwise +size+ is more efficient. + def length + load_target.size + end + + # Equivalent to collection.size.zero?. If the collection has + # not been already loaded and you are going to fetch the records anyway + # it is better to check collection.length.zero?. + def empty? + size.zero? + end + + def any? + if block_given? + method_missing(:any?) { |*block_args| yield(*block_args) } + else + !empty? + end + end + + def uniq(collection = self) + seen = Set.new + collection.inject([]) do |kept, record| + unless seen.include?(record.id) + kept << record + seen << record.id + end + kept + end + end + + # Replace this collection with +other_array+ + # This will perform a diff and delete/add only records that have changed. + def replace(other_array) + other_array.each { |val| raise_on_type_mismatch(val) } + + load_target + other = other_array.size < 100 ? other_array : other_array.to_set + current = @target.size < 100 ? @target : @target.to_set + + transaction do + delete(@target.select { |v| !other.include?(v) }) + concat(other_array.select { |v| !current.include?(v) }) + end + end + + def include?(record) + return false unless record.is_a?(@reflection.klass) + load_target if @reflection.options[:finder_sql] && !loaded? + return @target.include?(record) if loaded? + exists?(record) + end + + def proxy_respond_to?(method, include_private = false) + super || @reflection.klass.respond_to?(method, include_private) + end + + protected + def construct_find_options!(options) + end + + def load_target + if !@owner.new_record? || foreign_key_present + begin + if !loaded? + if @target.is_a?(Array) && @target.any? + @target = find_target + @target.find_all {|t| t.new_record? } + else + @target = find_target + end + end + rescue ActiveRecord::RecordNotFound + reset + end + end + + loaded if target + target + end + + def method_missing(method, *args) + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + if block_given? + super { |*block_args| yield(*block_args) } + else + super + end + elsif @reflection.klass.scopes.include?(method) + @reflection.klass.scopes[method].call(self, *args) + else + with_scope(construct_scope) do + if block_given? + @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } + else + @reflection.klass.send(method, *args) + end + end + end + end + + # overloaded in derived Association classes to provide useful scoping depending on association type. + def construct_scope + {} + end + + def reset_target! + @target = Array.new + end + + def find_target + records = + if @reflection.options[:finder_sql] + @reflection.klass.find_by_sql(@finder_sql) + else + find(:all) + end + + @reflection.options[:uniq] ? uniq(records) : records + end + + private + + def create_record(attrs) + attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) + ensure_owner_is_not_new + record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + @reflection.build_association(attrs) + end + if block_given? + add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } + else + add_record_to_target_with_callbacks(record) + end + end + + def build_record(attrs) + attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) + record = @reflection.build_association(attrs) + if block_given? + add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } + else + add_record_to_target_with_callbacks(record) + end + end + + def add_record_to_target_with_callbacks(record) + callback(:before_add, record) + yield(record) if block_given? + @target ||= [] unless loaded? + @target << record unless @reflection.options[:uniq] && @target.include?(record) + callback(:after_add, record) + record + end + + def callback(method, record) + callbacks_for(method).each do |callback| + ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record) + end + end + + def callbacks_for(callback_name) + full_callback_name = "#{callback_name}_for_#{@reflection.name}" + @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || [] + end + + def ensure_owner_is_not_new + if @owner.new_record? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + end + + def fetch_first_or_last_using_find?(args) + args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || + @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) + end + end + end +end