diff options
Diffstat (limited to 'activerecord/lib/active_record/relation/query_methods.rb')
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 116 |
1 files changed, 102 insertions, 14 deletions
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 670ba0987d..87dd513880 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -9,7 +9,8 @@ module ActiveRecord :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reorder_value, :reverse_order_value + :from_value, :reordering_value, :reverse_order_value, + :uniq_value, :references_values def includes(*args) args.reject! {|a| a.blank? } @@ -37,8 +38,26 @@ module ActiveRecord relation end + # Used to indicate that an association is referenced by an SQL string, and should + # therefore be JOINed in any query rather than loaded separately. + # + # For example: + # + # User.includes(:posts).where("posts.name = 'foo'") + # # => Doesn't JOIN the posts table, resulting in an error. + # + # User.includes(:posts).where("posts.name = 'foo'").references(:posts) + # # => Query now knows the string references posts, so adds a JOIN + def references(*args) + return self if args.blank? + + relation = clone + relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq + relation + end + # Works in two unique ways. - # + # # First: takes a block so it can be used just like Array#select. # # Model.scoped.select { |m| m.field == value } @@ -56,16 +75,16 @@ module ActiveRecord # array, it actually returns a relation object and can have other query # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. # - # This method will also take multiple parameters: + # The argument to the method can also be an array of fields. # - # >> Model.select(:field, :other_field, :and_one_more) + # >> Model.select([:field, :other_field, :and_one_more]) # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # - # Any attributes that do not have fields retrieved by a select - # will return `nil` when the getter method for that attribute is used: + # Accessing attributes of an object that do not have fields retrieved by a select + # will throw <tt>ActiveModel::MissingAttributeError</tt>: # # >> Model.select(:field).first.other_field - # => nil + # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(value = Proc.new) if block_given? to_a.select {|*block_args| value.call(*block_args) } @@ -87,16 +106,33 @@ module ActiveRecord def order(*args) return self if args.blank? + args = args.flatten + references = args.reject { |arg| Arel::Node === arg } + .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } + .compact + relation = clone - relation.order_values += args.flatten + relation = relation.references(references) if references.any? + relation.order_values += args relation end + # Replaces any existing order defined on the relation with the specified order. + # + # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC' + # + # Subsequent calls to order on the same relation will be appended. For example: + # + # User.order('email DESC').reorder('id ASC').order('name ASC') + # + # generates a query with 'ORDER BY id ASC, name ASC'. + # def reorder(*args) return self if args.blank? relation = clone - relation.reorder_value = args.flatten + relation.reordering_value = true + relation.order_values = args.flatten relation end @@ -121,6 +157,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.where_values += build_where(opts, rest) relation end @@ -129,6 +166,7 @@ module ActiveRecord return self if opts.blank? relation = clone + relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts relation.having_values += build_where(opts, rest) relation end @@ -158,6 +196,39 @@ module ActiveRecord relation end + # Returns a chainable relation with zero records, specifically an + # instance of the NullRelation class. + # + # The returned NullRelation inherits from Relation and implements the + # Null Object pattern so it is an object with defined null behavior: + # it always returns an empty array of records and does not query the database. + # + # Any subsequent condition chained to the returned relation will continue + # generating an empty relation and will not fire any query to the database. + # + # Used in cases where a method or scope could return zero records but the + # result needs to be chainable. + # + # For example: + # + # @posts = current_user.visible_posts.where(:name => params[:name]) + # # => the visible_posts method is expected to return a chainable Relation + # + # def visible_posts + # case role + # when 'Country Manager' + # Post.where(:country => country) + # when 'Reviewer' + # Post.published + # when 'Bad User' + # Post.none # => returning [] instead breaks the previous code + # end + # end + # + def none + NullRelation.new(@klass, @table) + end + def readonly(value = true) relation = clone relation.readonly_value = value @@ -176,9 +247,25 @@ module ActiveRecord relation end + # Specifies whether the records should be unique or not. For example: + # + # User.select(:name) + # # => Might return two records with the same name + # + # User.select(:name).uniq + # # => Returns 1 record per unique name + # + # User.select(:name).uniq.uniq(false) + # # => You can also remove the uniqueness + def uniq(value = true) + relation = clone + relation.uniq_value = value + relation + end + # Used to extend a scope with additional methods, either through - # a module or through a block provided. - # + # a module or through a block provided. + # # The object returned is a relation, which can be further extended. # # === Using a module @@ -200,7 +287,7 @@ module ActiveRecord # # scope = Model.scoped.extending do # def page(number) - # # pagination code goes here + # # pagination code goes here # end # end # scope.page(params[:page]) @@ -209,7 +296,7 @@ module ActiveRecord # # scope = Model.scoped.extending(Pagination) do # def per_page(number) - # # pagination code goes here + # # pagination code goes here # end # end def extending(*modules) @@ -246,12 +333,13 @@ module ActiveRecord arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - order = @reorder_value ? @reorder_value : @order_values + order = @order_values order = reverse_sql_order(order) if @reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? build_select(arel, @select_values.uniq) + arel.distinct(@uniq_value) arel.from(@from_value) if @from_value arel.lock(@lock_value) if @lock_value |