Skip to content

Commit 478f0aa

Browse files
neilshwekyp-mongo
andauthored
MONGOID-4547 Implement #take / #take! for AR feature parity (#5354)
* MONGOID-4547 add new error * MONGOID-4547 implement take/take! * MONGOID-4547 some cleanup * MONGOID-4547 don't use Mongo#first * MONGOID-4547 memory implementation * MONGOID-4547 add none implementation * MONGOID-4547 fix non implementation * MONGOID-4547 add documentation for take method * MONGOID-4547 add take! method docs * MONGOID-4547 docs changes * Apply suggestions from code review Co-authored-by: Oleg Pudeyev <[email protected]> * MONGOID-4547 change param to limit * Apply suggestions from code review Co-authored-by: Oleg Pudeyev <[email protected]> * MONGOID-4547 address comments * MONGOID-4547 fix document_not_found tests Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent fa04ed3 commit 478f0aa

File tree

12 files changed

+392
-3
lines changed

12 files changed

+392
-3
lines changed

docs/reference/queries.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,32 @@ Mongoid also has some helpful methods on criteria.
13901390
# expands out to "managers.name" in the query:
13911391
Band.all.pluck('managers.n')
13921392

1393+
* - ``Criteria#take``
1394+
1395+
*Get a list of n documents from the database or just one if no parameter
1396+
is provided.*
1397+
1398+
*This method does not apply a sort to the documents, so it can return
1399+
different document(s) than #first and #last.*
1400+
1401+
-
1402+
.. code-block:: ruby
1403+
1404+
Band.take
1405+
Band.take(5)
1406+
1407+
* - ``Criteria#take!``
1408+
1409+
*Get a document from the database or raise an error if none exist.*
1410+
1411+
*This method does not apply a sort to the documents, so it can return
1412+
different document(s) than #first and #last.*
1413+
1414+
-
1415+
.. code-block:: ruby
1416+
1417+
Band.take!
1418+
13931419
* - ``Criteria#tally``
13941420

13951421
*Get a mapping of values to counts for the provided field.*

docs/release-notes/mongoid-7.5.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,44 @@ return a ``Hash`` when instantiating a new document:
147147
band = Band.first
148148
p band.attributes.class
149149
# => BSON::Document
150+
151+
152+
Implemented ``Criteria#take/take!`` Method
153+
------------------------------------------
154+
155+
Mongoid 7.5 introduces the ``#take`` method which returns a document
156+
or a set of documents from the database without ordering by ``_id``:
157+
158+
.. code:: ruby
159+
160+
class Band
161+
include Mongoid::Document
162+
end
163+
164+
Band.create!
165+
Band.create!
166+
167+
Band.take
168+
# => #<Band _id: 62c835813282a4470c07d530, >
169+
Band.take(2)
170+
# => [ #<Band _id: 62c835813282a4470c07d530, >, #<Band _id: 62c835823282a4470c07d531, > ]
171+
172+
If a parameter is given to ``#take``, an array of documents is returned. If no parameter is
173+
given, a singular document is returned.
174+
175+
The ``#take!`` method functions the same as calling ``#take`` without arguments,
176+
but raises an DocumentNotFound error instead of returning nil if no documents
177+
are found.
178+
179+
.. code:: ruby
180+
181+
Band.take!
182+
# => #<Band _id: 62c835813282a4470c07d530, >
183+
184+
Note that the ``#take/take!`` methods do not apply a sort to the view before
185+
retrieving the documents from the database, and therefore they may return different
186+
results than the ``#first`` and ``#last`` methods. ``#take`` is equivalent to
187+
calling ``#first`` or ``#last`` with the ``{ id_sort: :none }`` option. This
188+
option has been deprecated in Mongoid 7.5 and it is recommended to use ``#take``
189+
instead going forward. Support for the ``:id_sort`` option will be dropped in
190+
Mongoid 8.

lib/config/locales/en.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,13 @@ en:
427427
\_\_\_\_\_\_default:\n
428428
\_\_\_\_\_\_\_\_hosts:\n
429429
\_\_\_\_\_\_\_\_\_\_- localhost:27017\n\n"
430+
no_documents_found:
431+
message: "Could not find a document of class %{klass}."
432+
summary: "Mongoid attempted to find a document of the class %{klass}
433+
but none exist."
434+
resolution: "Create a document of class %{klass} or use a finder
435+
method that returns nil when no documents are found instead of
436+
raising an exception."
430437
no_environment:
431438
message: "Could not load the configuration since no environment
432439
was defined."

lib/mongoid/contextual/memory.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,39 @@ def last
164164
eager_load([documents.last]).first
165165
end
166166

167+
# Take the given number of documents from the database.
168+
#
169+
# @example Take a document.
170+
# context.take
171+
#
172+
# @param [ Integer | nil ] limit The number of documents to take or nil.
173+
#
174+
# @return [ Document ] The document.
175+
def take(limit = nil)
176+
if limit
177+
eager_load(documents.take(limit))
178+
else
179+
eager_load([documents.first]).first
180+
end
181+
end
182+
183+
# Take the given number of documents from the database.
184+
#
185+
# @example Take a document.
186+
# context.take
187+
#
188+
# @return [ Document ] The document.
189+
#
190+
# @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
191+
# documents to take.
192+
def take!
193+
if documents.empty?
194+
raise Errors::DocumentNotFound.new(klass, nil, nil)
195+
else
196+
eager_load([documents.first]).first
197+
end
198+
end
199+
167200
# Get the length of matching documents in the context.
168201
#
169202
# @example Get the length of matching documents.

lib/mongoid/contextual/mongo.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,44 @@ def limit(value)
400400
@view = view.limit(value) and self
401401
end
402402

403+
# Take the given number of documents from the database.
404+
#
405+
# @example Take 10 documents
406+
# context.take(10)
407+
#
408+
# @param [ Integer | nil ] limit The number of documents to return or nil.
409+
#
410+
# @return [ Document | Array<Document> ] The list of documents, or one
411+
# document if no value was given.
412+
def take(limit = nil)
413+
if limit
414+
limit(limit).to_a
415+
else
416+
# Do to_a first so that the Mongo#first method is not used and the
417+
# result is not sorted.
418+
limit(1).to_a.first
419+
end
420+
end
421+
422+
# Take one document from the database and raise an error if there are none.
423+
#
424+
# @example Take a document
425+
# context.take!
426+
#
427+
# @return [ Document ] The document.
428+
#
429+
# @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
430+
# documents to take.
431+
def take!
432+
# Do to_a first so that the Mongo#first method is not used and the
433+
# result is not sorted.
434+
if fst = limit(1).to_a.first
435+
fst
436+
else
437+
raise Errors::DocumentNotFound.new(klass, nil, nil)
438+
end
439+
end
440+
403441
# Initiate a map/reduce operation from the context.
404442
#
405443
# @example Initiate a map/reduce.

lib/mongoid/contextual/none.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ def initialize(criteria)
125125
# @return [ nil ] Always nil.
126126
def last; nil; end
127127

128+
# Returns nil or empty array.
129+
#
130+
# @example Take a document in null context.
131+
# context.take
132+
#
133+
# @param [ Integer | nil ] limit The number of documents to take or nil.
134+
#
135+
# @return [ [] | nil ] Empty array or nil.
136+
def take(limit = nil)
137+
limit ? [] : nil
138+
end
139+
140+
# Always raises an error.
141+
#
142+
# @example Take a document in null context.
143+
# context.take!
144+
#
145+
# @raises [ Mongoid::Errors::DocumentNotFound ] always raises.
146+
def take!
147+
raise Errors::DocumentNotFound.new(klass, nil, nil)
148+
end
149+
128150
# Always returns zero.
129151
#
130152
# @example Get the length of null context.

lib/mongoid/errors/document_not_found.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class DocumentNotFound < MongoidError
2424
# there is a shard key this will be a hash.
2525
def initialize(klass, params, unmatched = nil)
2626
if !unmatched && !params.is_a?(Hash)
27-
unmatched = Array(params)
27+
unmatched = Array(params) if params
2828
end
2929

3030
@klass, @params = klass, params
@@ -98,7 +98,9 @@ def total(params)
9898
#
9999
# @return [ String ] The problem.
100100
def message_key(params, unmatched)
101-
if Hash === params
101+
if !params && !unmatched
102+
"no_documents_found"
103+
elsif Hash === params
102104
"document_with_attributes_not_found"
103105
elsif Hash === unmatched && unmatched.size >= 2
104106
"document_with_shard_key_not_found"

lib/mongoid/findable.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ module Findable
4141
:pluck,
4242
:read,
4343
:sum,
44+
:take,
45+
:take!,
46+
:tally,
4447
:text_search,
4548
:update,
4649
:update_all,
47-
:tally,
4850

4951
# Returns a count of records in the database.
5052
# If you want to specify conditions use where.

spec/mongoid/contextual/memory_spec.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,108 @@
641641
end
642642
end
643643

644+
describe "#take" do
645+
646+
let(:hobrecht) do
647+
Address.new(street: "hobrecht")
648+
end
649+
650+
let(:friedel) do
651+
Address.new(street: "friedel")
652+
end
653+
654+
let(:criteria) do
655+
Address.where(:street.in => [ "hobrecht", "friedel" ]).tap do |crit|
656+
crit.documents = [ hobrecht, friedel ]
657+
end
658+
end
659+
660+
let(:context) do
661+
described_class.new(criteria)
662+
end
663+
664+
it "returns the first matching document" do
665+
expect(context.take).to eq(hobrecht)
666+
end
667+
668+
it "returns an array when passing a limit" do
669+
expect(context.take(2)).to eq([ hobrecht, friedel ])
670+
end
671+
672+
it "returns an array when passing a limit as 1" do
673+
expect(context.take(1)).to eq([ hobrecht ])
674+
end
675+
676+
context 'when there is a collation on the criteria' do
677+
678+
let(:criteria) do
679+
Address.where(:street.in => [ "hobrecht", "friedel" ]).tap do |crit|
680+
crit.documents = [ hobrecht, friedel ]
681+
end.collation(locale: 'en_US', strength: 2)
682+
end
683+
684+
it "raises an exception" do
685+
expect {
686+
context.take
687+
}.to raise_exception(Mongoid::Errors::InMemoryCollationNotSupported)
688+
end
689+
end
690+
end
691+
692+
describe "#take!" do
693+
694+
let(:hobrecht) do
695+
Address.new(street: "hobrecht")
696+
end
697+
698+
let(:friedel) do
699+
Address.new(street: "friedel")
700+
end
701+
702+
let(:criteria) do
703+
Address.where(:street.in => [ "hobrecht", "friedel" ]).tap do |crit|
704+
crit.documents = [ hobrecht, friedel ]
705+
end
706+
end
707+
708+
let(:context) do
709+
described_class.new(criteria)
710+
end
711+
712+
it "returns the first matching document" do
713+
expect(context.take!).to eq(hobrecht)
714+
end
715+
716+
context "when the criteria is empty" do
717+
let(:criteria) do
718+
Address.where(street: "bogus").tap do |crit|
719+
crit.documents = []
720+
end
721+
end
722+
723+
it "raise an error" do
724+
expect do
725+
context.take!
726+
end.to raise_error(Mongoid::Errors::DocumentNotFound, /Could not find a document of class Address./)
727+
end
728+
end
729+
730+
context 'when there is a collation on the criteria' do
731+
732+
let(:criteria) do
733+
Address.where(:street.in => [ "hobrecht", "friedel" ]).tap do |crit|
734+
crit.documents = [ hobrecht, friedel ]
735+
end.collation(locale: 'en_US', strength: 2)
736+
end
737+
738+
it "raises an exception" do
739+
expect {
740+
context.take
741+
}.to raise_exception(Mongoid::Errors::InMemoryCollationNotSupported)
742+
end
743+
end
744+
end
745+
644746
describe "#initialize" do
645747

646748
context "when the criteria has no options" do

0 commit comments

Comments
 (0)