diff options
Diffstat (limited to 'activerecord/lib/active_record/relation.rb')
-rw-r--r-- | activerecord/lib/active_record/relation.rb | 225 |
1 files changed, 201 insertions, 24 deletions
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 853103a606..530402bf5d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,8 +1,8 @@ module ActiveRecord class Relation delegate :to_sql, :to => :relation - delegate :length, :collect, :find, :map, :each, :to => :to_a - attr_reader :relation, :klass + delegate :length, :collect, :map, :each, :to => :to_a + attr_reader :relation, :klass, :associations_to_preload, :eager_load_associations def initialize(klass, relation, readonly = false, preload = [], eager_load = []) @klass, @relation = klass, relation @@ -12,6 +12,21 @@ module ActiveRecord @loaded = false end + def merge(r) + joins(r.relation.joins(r.relation)). + group(r.send(:group_clauses).join(', ')). + order(r.send(:order_clauses).join(', ')). + where(r.send(:where_clause)). + limit(r.taken). + offset(r.skipped). + select(r.send(:select_clauses).join(', ')). + eager_load(r.eager_load_associations). + preload(r.associations_to_preload). + from(r.send(:sources).any? ? r.send(:from_clauses) : nil) + end + + alias :& :merge + def preload(*associations) create_new_relation(@relation, @readonly, @associations_to_preload + Array.wrap(associations)) end @@ -25,23 +40,39 @@ module ActiveRecord end def select(selects) - create_new_relation(@relation.project(selects)) + selects.present? ? create_new_relation(@relation.project(selects)) : create_new_relation + end + + def from(from) + from.present? ? create_new_relation(@relation.from(from)) : create_new_relation end def group(groups) - create_new_relation(@relation.group(groups)) + groups.present? ? create_new_relation(@relation.group(groups)) : create_new_relation end def order(orders) - create_new_relation(@relation.order(orders)) + orders.present? ? create_new_relation(@relation.order(orders)) : create_new_relation + end + + def reverse_order + relation = create_new_relation + relation.instance_variable_set(:@orders, nil) + + order_clause = @relation.send(:order_clauses).join(', ') + if order_clause.present? + relation.order(reverse_sql_order(order_clause)) + else + relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC") + end end def limit(limits) - create_new_relation(@relation.take(limits)) + limits.present? ? create_new_relation(@relation.take(limits)) : create_new_relation end def offset(offsets) - create_new_relation(@relation.skip(offsets)) + offsets.present? ? create_new_relation(@relation.skip(offsets)) : create_new_relation end def on(join) @@ -49,22 +80,27 @@ module ActiveRecord end def joins(join, join_type = nil) - 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 + return create_new_relation 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(join, join_type) + @relation.join(@klass.send(:build_association_joins, join)) + end + else + @relation.join(join, join_type) end - create_new_relation(join) + + create_new_relation(join_relation) end def where(*args) + return create_new_relation if args.blank? + if [String, Hash, Array].include?(args.first.class) conditions = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) else @@ -88,9 +124,10 @@ module ActiveRecord :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 + :offset => @relation.skipped, + :from => (@relation.send(:from_clauses) if @relation.send(:sources).any?) }, ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil)) end @@ -108,6 +145,31 @@ module ActiveRecord alias all to_a + def find(*ids, &block) + return to_a.find(&block) if block_given? + + 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 exists?(id = nil) + relation = select("#{@klass.quoted_table_name}.#{@klass.primary_key}").limit(1) + relation = relation.where(@klass.primary_key => id) if id + relation.first ? true : false + end + def first if loaded? @records.first @@ -116,30 +178,145 @@ module ActiveRecord end end + def last + if loaded? + @records.last + else + @last ||= reverse_order.limit(1).to_a[0] + end + end + + def destroy_all + to_a.each {|object| object.destroy} + reset + end + def loaded? @loaded end def reload @loaded = false - @records = @first = nil + reset + end + + def reset + @first = @last = nil + @records = [] self end - private + 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 create_new_relation(relation, readonly = @readonly, preload = @associations_to_preload, eager_load = @eager_load_associations) - Relation.new(@klass, relation, readonly, preload, eager_load) + 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 + result + end + end + + 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 + 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 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 + + def find_some(ids) + result = where(@klass.primary_key => ids).all + + expected_size = + if @relation.taken && ids.size > @relation.taken + @relation.taken + else + 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 = @relation, readonly = @readonly, preload = @associations_to_preload, eager_load = @eager_load_associations) + self.class.new(@klass, relation, readonly, preload, eager_load) + end + + def where_clause(join_string = "\n\tAND ") + @relation.send(:where_clauses).join(join_string) + end + + def reverse_sql_order(order_query) + order_query.to_s.split(/,/).each { |s| + if s.match(/\s(asc|ASC)$/) + s.gsub!(/\s(asc|ASC)$/, ' DESC') + elsif s.match(/\s(desc|DESC)$/) + s.gsub!(/\s(desc|DESC)$/, ' ASC') + else + s.concat(' DESC') + end + }.join(',') end end |