diff options
Diffstat (limited to 'activerecord/lib/active_record/relation.rb')
-rw-r--r-- | activerecord/lib/active_record/relation.rb | 253 |
1 files changed, 189 insertions, 64 deletions
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 5f0eec754f..c927270eee 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -4,37 +4,94 @@ module ActiveRecord delegate :length, :collect, :find, :map, :each, :to => :to_a attr_reader :relation, :klass - def initialize(klass, relation) + def initialize(klass, relation, readonly = false, preload = [], eager_load = []) @klass, @relation = klass, relation - @readonly = false - @associations_to_preload = [] - @eager_load_associations = [] + @readonly = readonly + @associations_to_preload = preload + @eager_load_associations = eager_load + @loaded = false end - def preload(association) - @associations_to_preload += association - self + def preload(*associations) + create_new_relation(@relation, @readonly, @associations_to_preload + Array.wrap(associations)) end - def eager_load(association) - @eager_load_associations += association - self + def eager_load(*associations) + create_new_relation(@relation, @readonly, @associations_to_preload, @eager_load_associations + Array.wrap(associations)) end def readonly - @readonly = true - self + create_new_relation(@relation, true) + end + + def select(selects) + create_new_relation(@relation.project(selects)) + end + + def group(groups) + create_new_relation(@relation.group(groups)) + end + + def order(orders) + create_new_relation(@relation.order(orders)) + end + + def limit(limits) + create_new_relation(@relation.take(limits)) + end + + def offset(offsets) + create_new_relation(@relation.skip(offsets)) + end + + def on(join) + create_new_relation(@relation.on(join)) + end + + def joins(join, join_type = nil) + return self if join.blank? + + join_relation = case join + when String + @relation.join(join) + when Hash, Array, Symbol + if @klass.send(:array_of_strings?, join) + @relation.join(join.join(' ')) + else + @relation.join(@klass.send(:build_association_joins, join)) + end + else + @relation.join(join, join_type) + end + + create_new_relation(join_relation) + end + + def where(*args) + if [String, Hash, Array].include?(args.first.class) + conditions = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) + else + conditions = args.first + end + + create_new_relation(@relation.where(conditions)) + end + + def respond_to?(method) + @relation.respond_to?(method) || Array.method_defined?(method) || super end def to_a - records = if @eager_load_associations.any? + return @records if loaded? + + @records = if @eager_load_associations.any? catch :invalid_query do return @klass.send(:find_with_associations, { :select => @relation.send(:select_clauses).join(', '), :joins => @relation.joins(relation), :group => @relation.send(:group_clauses).join(', '), :order => @relation.send(:order_clauses).join(', '), - :conditions => @relation.send(:where_clauses).join("\n\tAND "), + :conditions => where_clause, :limit => @relation.taken, :offset => @relation.skipped }, @@ -45,83 +102,151 @@ module ActiveRecord @klass.find_by_sql(@relation.to_sql) end - @klass.send(:preload_associations, records, @associations_to_preload) unless @associations_to_preload.empty? - records.each { |record| record.readonly! } if @readonly + @associations_to_preload.each {|associations| @klass.send(:preload_associations, @records, associations) } + @records.each { |record| record.readonly! } if @readonly - records + @loaded = true + @records end - def first - @relation = @relation.take(1) - to_a.first - end + alias all to_a - def select(selects) - selects.blank? ? self : Relation.new(@klass, @relation.project(selects)) - end + def find(*ids, &block) + return to_a.find(&block) if block_given? - def group(groups) - groups.blank? ? self : Relation.new(@klass, @relation.group(groups)) + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? + + ids = ids.flatten.compact.uniq + + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + when 1 + result = find_one(ids.first) + expects_array ? [ result ] : result + else + find_some(ids) + end end - def order(orders) - orders.blank? ? self : Relation.new(@klass, @relation.order(orders)) + def first + if loaded? + @records.first + else + @first ||= limit(1).to_a[0] + end end - def limit(limits) - limits.blank? ? self : Relation.new(@klass, @relation.take(limits)) + def loaded? + @loaded end - def offset(offsets) - offsets.blank? ? self : Relation.new(@klass, @relation.skip(offsets)) + def reload + @loaded = false + @records = @first = nil + self end - def on(join) - join.blank? ? self : Relation.new(@klass, @relation.on(join)) + protected + + def method_missing(method, *args, &block) + if @relation.respond_to?(method) + @relation.send(method, *args, &block) + elsif Array.method_defined?(method) + to_a.send(method, *args, &block) + elsif match = DynamicFinderMatch.match(method) + attributes = match.attribute_names + super unless @klass.send(:all_attributes_exists?, attributes) + + if match.finder? + find_by_attributes(match, attributes, *args) + elsif match.instantiator? + find_or_instantiator_by_attributes(match, attributes, *args, &block) + end + else + super + end end - def joins(join, join_type = nil) - if join.blank? - self + def find_by_attributes(match, attributes, *args) + conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h} + result = where(conditions).send(match.finder) + + if match.bang? && result.blank? + raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" else - join = case join - when String - @relation.join(join) - when Hash, Array, Symbol - if @klass.send(:array_of_strings?, join) - @relation.join(join.join(' ')) - else - @relation.join(@klass.send(:build_association_joins, join)) - end - else - @relation.join(join, join_type) - end - Relation.new(@klass, join) + result end end - def conditions(conditions) - if conditions.blank? - self + def find_or_instantiator_by_attributes(match, attributes, *args) + guard_protected_attributes = false + + if args[0].is_a?(Hash) + guard_protected_attributes = true + attributes_for_create = args[0].with_indifferent_access + conditions = attributes_for_create.slice(*attributes).symbolize_keys else - conditions = @klass.send(:merge_conditions, conditions) if [String, Hash, Array].include?(conditions.class) - Relation.new(@klass, @relation.where(conditions)) + attributes_for_create = conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h} end + + record = where(conditions).first + + unless record + record = @klass.new { |r| r.send(:attributes=, attributes_for_create, guard_protected_attributes) } + yield(record) if block_given? + record.save if match.instantiator == :create + end + + record end - def respond_to?(method) - @relation.respond_to?(method) || Array.method_defined?(method) || super + def find_one(id) + record = where(@klass.primary_key => id).first + + unless record + conditions = where_clause(', ') + conditions = " [WHERE #{conditions}]" if conditions.present? + raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" + end + + record end - private - def method_missing(method, *args, &block) - if @relation.respond_to?(method) - @relation.send(method, *args, &block) - elsif Array.method_defined?(method) - to_a.send(method, *args, &block) + def find_some(ids) + result = where(@klass.primary_key => ids).all + + expected_size = + if @relation.taken && ids.size > @relation.taken + @relation.taken else - super + ids.size end + + # 11 ids with limit 3, offset 9 should give 2 results. + if @relation.skipped && (ids.size - @relation.skipped < expected_size) + expected_size = ids.size - @relation.skipped + end + + if result.size == expected_size + result + else + conditions = where_clause(', ') + conditions = " [WHERE #{conditions}]" if conditions.present? + + error = "Couldn't find all #{@klass.name.pluralize} with IDs " + error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})" + raise RecordNotFound, error end + end + + def create_new_relation(relation, readonly = @readonly, preload = @associations_to_preload, eager_load = @eager_load_associations) + Relation.new(@klass, relation, readonly, preload, eager_load) + end + + def where_clause(join_string = "\n\tAND ") + @relation.send(:where_clauses).join(join_string) + end end end |