Skip to content
This repository
Browse code

Added `none` query method to return zero records.

And added NullRelation class implementing the null object pattern for the `Relation` class.
  • Loading branch information...
commit 8270e4a8ce8337fca98030953922e5992b06a3dd 1 parent bb842e8
Juanjo Bazán authored January 30, 2012
13  activerecord/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,18 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   Implemented ActiveRecord::Relation#none method
  4
+
  5
+    The `none` method returns a chainable relation with zero records
  6
+    (an instance of the NullRelation class).
  7
+
  8
+    Any subsequent condition chained to the returned relation will continue
  9
+    generating an empty relation and will not fire any query to the database.
  10
+
  11
+    *Juanjo Bazán*
  12
+
  13
+*   Added the `ActiveRecord::NullRelation` class implementing the null
  14
+    object pattern for the Relation class. *Juanjo Bazán*
  15
+
3 16
 *   Added deprecation for the `:dependent => :restrict` association option.
4 17
 
5 18
     Please note:
1  activerecord/lib/active_record.rb
@@ -43,6 +43,7 @@ module ActiveRecord
43 43
     autoload :AutosaveAssociation
44 44
 
45 45
     autoload :Relation
  46
+    autoload :NullRelation
46 47
 
47 48
     autoload_under 'relation' do
48 49
       autoload :QueryMethods
10  activerecord/lib/active_record/null_relation.rb
... ...
@@ -0,0 +1,10 @@
  1
+# -*- coding: utf-8 -*-
  2
+
  3
+module ActiveRecord
  4
+  # = Active Record Null Relation
  5
+  class NullRelation < Relation
  6
+    def exec_queries
  7
+      @records = []
  8
+    end
  9
+  end
  10
+end
2  activerecord/lib/active_record/querying.rb
@@ -8,7 +8,7 @@ module Querying
8 8
     delegate :find_each, :find_in_batches, :to => :scoped
9 9
     delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
10 10
              :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
11  
-             :having, :create_with, :uniq, :references, :to => :scoped
  11
+             :having, :create_with, :uniq, :references, :none, :to => :scoped
12 12
     delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
13 13
 
14 14
     # Executes a custom SQL query against your database and returns all the results. The results will
33  activerecord/lib/active_record/relation/query_methods.rb
@@ -196,6 +196,39 @@ def lock(locks = true)
196 196
       relation
197 197
     end
198 198
 
  199
+    # Returns a chainable relation with zero records, specifically an
  200
+    # instance of the NullRelation class.
  201
+    #
  202
+    # The returned NullRelation inherits from Relation and implements the
  203
+    # Null Object pattern so it is an object with defined null behavior:
  204
+    # it always returns an empty array of records and avoids any database query.
  205
+    #
  206
+    # Any subsequent condition chained to the returned relation will continue
  207
+    # generating an empty relation and will not fire any query to the database.
  208
+    #
  209
+    # Used in cases where is needed a method or a scope that could return zero
  210
+    # results but the response has to be chainable.
  211
+    #
  212
+    # For example:
  213
+    #
  214
+    #   @posts = current_user.visible_posts.where(:name => params[:name])
  215
+    #   # => the visible_post method response has to be a chainable Relation
  216
+    #
  217
+    #   def visible_posts
  218
+    #     case role
  219
+    #     if 'Country Manager'
  220
+    #       Post.where(:country => country)
  221
+    #     if 'Reviewer'
  222
+    #       Post.published
  223
+    #     if 'Bad User'
  224
+    #       Post.none # => returning [] instead breaks the previous code
  225
+    #     end
  226
+    #   end
  227
+    #
  228
+    def none
  229
+      NullRelation.new(@klass, @table)
  230
+    end
  231
+
199 232
     def readonly(value = true)
200 233
       relation = clone
201 234
       relation.readonly_value = value
13  activerecord/test/cases/relations_test.rb
@@ -215,6 +215,19 @@ def test_select_with_block
215 215
     assert_equal [2, 4, 6, 8, 10], even_ids.sort
216 216
   end
217 217
 
  218
+  def test_none
  219
+    assert_no_queries do
  220
+      assert_equal [], Developer.none
  221
+      assert_equal [], Developer.scoped.none
  222
+    end
  223
+  end
  224
+
  225
+  def test_none_chainable
  226
+    assert_no_queries do
  227
+      assert_equal [], Developer.none.where(:name => 'David')
  228
+    end
  229
+  end
  230
+
218 231
   def test_joins_with_nil_argument
219 232
     assert_nothing_raised { DependentFirm.joins(nil).first }
220 233
   end

2 notes on commit 8270e4a

José Valim
Owner

I love this pull request but while trying it out I have noticed there is still a long way to go. Many relation methods that does a query straight to the database need to overridden, otherwise none doesn't work as expected. For example, Post.none.pluck(:id) should return an empty array, but it doesn't. I guess other methods like update_all, delete will all have the same issue. /cc @jonleighton

Juanjo Bazán

Thanks @josevalim
You are right, I see a couple of options here:

1) we can modify every method doing straight database queries (select_all, update_all, delete_all, etc...). That would solve the problem, but it'll modify core methods (like execute) and probably we'll need to change every database adapter.

2) We can add a false condition to the NullRelation class (something like where('1<0')) so once none is called if you fire a query directly to the db, it will always return zero records.

I'm +1 for this second option, it is simpler and keeps all none-related code in the NullRelation class. Here is the pull request for it:
#5590

Please sign in to comment.
Something went wrong with that request. Please try again.