Skip to content

Commit cb8418c

Browse files
neilshwekyp-mongo
andauthored
MONGOID-4998 deprecate :id_sort and implement limit positional argument on #first/#last (#5358)
* MONGOID-4998 add limit to #first * MOGNOID-4998 add none implementation * MONGOID-4998 add caching and limit to last, fix first tests * MONGOID-4998 pass options through on memory, none, findable * MONGOID-4998 add last findable tests * Apply suggestions from code review * MONGOID-4998 add mongoid warning module * MONGOID-4998 make limit a positional argument * MONGOID-4998 update last signatures * MONGOID-4998 fix last sig * MONGOID-4998 fix mongo tests * MONGOID-4998 fix tests * MONGOID-4998 fix enumerable test * MONGOID-4998 add release note * MONGOID-4998 refactor numbered_cache * MONGOID-4998 add back id_sort functionality * MONGOID-4998 update queries.txt * MONGOID-4998 np docs * MONGOID-4998 fix test * Update docs/reference/queries.txt Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * Update docs/reference/queries.txt Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * Update docs/release-notes/mongoid-7.5.txt Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * Update docs/release-notes/mongoid-7.5.txt Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * Update lib/mongoid/warnings.rb Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com> * MONGOID-4998 fix up suggestions * MONGOID-4998 use nil? * MONGOID-4998 use nil? on last Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com>
1 parent c87c00e commit cb8418c

File tree

14 files changed

+1042
-62
lines changed

14 files changed

+1042
-62
lines changed

docs/reference/queries.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,15 +1301,17 @@ Mongoid also has some helpful methods on criteria.
13011301

13021302
* - ``Criteria#first|last``
13031303

1304-
*Finds a single document given the provided criteria. This automatically adds a sort on id.
1305-
Opt out of adding the id sort with the {id_sort: :none} option.*
1304+
*Finds a single document given the provided criteria. This automatically adds a sort on _id.
1305+
Opt out of adding the id sort with the {id_sort: :none} option. This option is deprecated -
1306+
please call ``take`` instead. Get a list of documents by passing in a limit argument.*
13061307

13071308
-
13081309
.. code-block:: ruby
13091310

13101311
Band.first
13111312
Band.where(:members.with_size => 3).first
13121313
Band.where(:members.with_size => 3).last
1314+
Band.first(2)
13131315

13141316
* - ``Criteria#first_or_create``
13151317

docs/release-notes/mongoid-7.5.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,37 @@ calling ``#first`` or ``#last`` with the ``{ id_sort: :none }`` option. This
188188
option has been deprecated in Mongoid 7.5 and it is recommended to use ``#take``
189189
instead going forward. Support for the ``:id_sort`` option will be dropped in
190190
Mongoid 8.
191+
192+
193+
Deprecate ``:id_sort`` Option and Support ``limit`` on ``#first/last``
194+
----------------------------------------------------------------------
195+
196+
Mongoid 7.5 deprecates the ``:id_sort`` keyword argument for the
197+
``Criteria#first`` and ``Criteria#last`` methods. Please use ``Criteria#take``
198+
to retrieve documents without sorting by id.
199+
200+
The ``first`` and ``last`` methods now take the number of documents to return
201+
as a positional argument, mirroring the functionality of Ruby's ``Enumerable``
202+
method and ActiveRecord's ``first`` and ``last`` methods. Both invocations
203+
(with limit as a positional arguments and with the ``:id_sort`` option) remain
204+
supported in Mongoid 7.x, but the ``:id_sort`` invocation will be removed in
205+
Mongoid 8.
206+
207+
.. code:: ruby
208+
209+
Band.first
210+
# => #<Band _id: 62c835813282a4470c07d530, >
211+
Band.first(2)
212+
# => [ #<Band _id: 62c835813282a4470c07d530, >, #<Band _id: 62c835823282a4470c07d531, > ]
213+
Band.last
214+
# => #<Band _id: 62c835823282a4470c07d531, >
215+
Band.last(2)
216+
# => [#<Band _id: 62c835813282a4470c07d530, >, #<Band _id: 62c835823282a4470c07d531, >]
217+
218+
When providing a limit, ``#first/last`` will return a list of documents, and
219+
when not providing a limit (or providing ``nil``), a single document will be
220+
returned.
221+
222+
Note that the ``#first/last`` methods apply a sort on ``_id``, which can
223+
cause performance issues. To get a document without sorting first, use the
224+
``Critera#take`` method.

lib/mongoid.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
require "mongoid/document"
2525
require "mongoid/tasks/database"
2626
require "mongoid/query_cache"
27+
require "mongoid/warnings"
2728

2829
# If we are using Rails then we will include the Mongoid railtie. This has all
2930
# the nifty initializers that Mongoid needs.

lib/mongoid/association/referenced/has_many/enumerable.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,16 @@ def any?(*args)
243243
# use the option { id_sort: :none }.
244244
# Be aware that #first/#last won't guarantee order in this case.
245245
#
246-
# @param [ Hash ] opts The options for the query returning the first document.
246+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
247+
# return, or a hash of options.
247248
#
248-
# @option opts [ :none ] :id_sort Don't apply a sort on _id.
249+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
250+
# Don't apply a sort on _id if no other sort is defined on the criteria.
249251
#
250252
# @return [ Document ] The first document found.
251-
def first(opts = {})
253+
def first(limit_or_opts = nil)
252254
_loaded.try(:values).try(:first) ||
253-
_added[(ul = _unloaded.try(:first, opts)).try(:_id)] ||
255+
_added[(ul = _unloaded.try(:first, limit_or_opts)).try(:_id)] ||
254256
ul ||
255257
_added.values.try(:first)
256258
end
@@ -330,15 +332,17 @@ def in_memory
330332
# use the option { id_sort: :none }.
331333
# Be aware that #first/#last won't guarantee order in this case.
332334
#
333-
# @param [ Hash ] opts The options for the query returning the first document.
335+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
336+
# return, or a hash of options.
334337
#
335-
# @option opts [ :none ] :id_sort Don't apply a sort on _id.
338+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
339+
# Don't apply a sort on _id if no other sort is defined on the criteria.
336340
#
337341
# @return [ Document ] The last document found.
338-
def last(opts = {})
342+
def last(limit_or_opts = nil)
339343
_added.values.try(:last) ||
340344
_loaded.try(:values).try(:last) ||
341-
_added[(ul = _unloaded.try(:last, opts)).try(:_id)] ||
345+
_added[(ul = _unloaded.try(:last, limit_or_opts)).try(:_id)] ||
342346
ul
343347
end
344348

lib/mongoid/contextual/memory.rb

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,16 @@ def exists?
116116
# @example Get the first document.
117117
# context.first
118118
#
119+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
120+
# return, or a hash of options.
121+
#
119122
# @return [ Document ] The first document.
120-
def first(*args)
121-
eager_load([documents.first]).first
123+
def first(limit_or_opts = nil)
124+
if limit_or_opts.nil? || limit_or_opts.is_a?(Hash)
125+
eager_load([documents.first]).first
126+
else
127+
eager_load(documents.first(limit_or_opts))
128+
end
122129
end
123130
alias :one :first
124131
alias :find_first :first
@@ -159,9 +166,19 @@ def inc(incs)
159166
# @example Get the last document.
160167
# context.last
161168
#
169+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
170+
# return, or a hash of options.
171+
#
172+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
173+
# Don't apply a sort on _id if no other sort is defined on the criteria.
174+
#
162175
# @return [ Document ] The last document.
163-
def last
164-
eager_load([documents.last]).first
176+
def last(limit_or_opts = nil)
177+
if limit_or_opts.nil? || limit_or_opts.is_a?(Hash)
178+
eager_load([documents.last]).first
179+
else
180+
eager_load(documents.last(limit_or_opts))
181+
end
165182
end
166183

167184
# Take the given number of documents from the database.

lib/mongoid/contextual/mongo.rb

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -253,25 +253,28 @@ def find_one_and_delete
253253
# and have no sort defined on the criteria, use the option { id_sort: :none }.
254254
# Be aware that #first/#last won't guarantee order in this case.
255255
#
256-
# @param [ Hash ] opts The options for the query returning the first document.
256+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
257+
# return, or a hash of options.
257258
#
258-
# @option opts [ :none ] :id_sort Don't apply a sort on _id if no other sort
259-
# is defined on the criteria.
259+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
260+
# Don't apply a sort on _id if no other sort is defined on the criteria.
260261
#
261262
# @return [ Document ] The first document.
262-
def first(opts = {})
263-
return documents.first if cached? && cache_loaded?
264-
try_cache(:first) do
265-
if sort = view.sort || ({ _id: 1 } unless opts[:id_sort] == :none)
266-
if raw_doc = view.sort(sort).limit(1).first
267-
doc = Factory.from_db(klass, raw_doc, criteria)
268-
eager_load([doc]).first
269-
end
270-
else
271-
if raw_doc = view.limit(1).first
272-
doc = Factory.from_db(klass, raw_doc, criteria)
273-
eager_load([doc]).first
274-
end
263+
def first(limit_or_opts = nil)
264+
limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
265+
if cached? && cache_loaded?
266+
return limit ? documents.first(limit) : documents.first
267+
end
268+
try_numbered_cache(:first, limit) do
269+
if limit_or_opts.try(:key?, :id_sort)
270+
Mongoid::Warnings.warn_id_sort_deprecated
271+
end
272+
sorted_view = view
273+
if sort = view.sort || ({ _id: 1 } unless limit_or_opts.try(:fetch, :id_sort) == :none)
274+
sorted_view = view.sort(sort)
275+
end
276+
if raw_docs = sorted_view.limit(limit || 1).to_a
277+
process_raw_docs(raw_docs, limit)
275278
end
276279
end
277280
end
@@ -362,19 +365,26 @@ def initialize(criteria)
362365
# and have no sort defined on the criteria, use the option { id_sort: :none }.
363366
# Be aware that #first/#last won't guarantee order in this case.
364367
#
365-
# @param [ Hash ] opts The options for the query returning the first document.
368+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
369+
# return, or a hash of options.
366370
#
367-
# @option opts [ :none ] :id_sort Don't apply a sort on _id if no other sort
368-
# is defined on the criteria.
369-
def last(opts = {})
370-
try_cache(:last) do
371-
with_inverse_sorting(opts) do
372-
if raw_doc = view.limit(1).first
373-
doc = Factory.from_db(klass, raw_doc, criteria)
374-
eager_load([doc]).first
371+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
372+
# Don't apply a sort on _id if no other sort is defined on the criteria.
373+
#
374+
# @return [ Document ] The last document.
375+
def last(limit_or_opts = nil)
376+
limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
377+
if cached? && cache_loaded?
378+
return limit ? documents.last(limit) : documents.last
379+
end
380+
res = try_numbered_cache(:last, limit) do
381+
with_inverse_sorting(limit_or_opts) do
382+
if raw_docs = view.limit(limit || 1).to_a
383+
process_raw_docs(raw_docs, limit)
375384
end
376385
end
377386
end
387+
res.is_a?(Array) ? res.reverse : res
378388
end
379389

380390
# Returns the number of documents in the database matching
@@ -644,6 +654,31 @@ def try_cache(key, &block)
644654
end
645655
end
646656

657+
# yield the block given or return the cached value
658+
#
659+
# @param [ String, Symbol ] key The instance variable name
660+
# @param [ Integer | nil ] n The number of documents requested or nil
661+
# if none is requested.
662+
#
663+
# @return [ Object ] The result of the block.
664+
def try_numbered_cache(key, n, &block)
665+
unless cached?
666+
yield if block_given?
667+
else
668+
len = n || 1
669+
ret = instance_variable_get("@#{key}")
670+
if !ret || ret.length < len
671+
instance_variable_set("@#{key}", ret = Array.wrap(yield))
672+
elsif !n
673+
ret.is_a?(Array) ? ret.first : ret
674+
elsif ret.length > len
675+
ret.first(n)
676+
else
677+
ret
678+
end
679+
end
680+
end
681+
647682
# Update the documents for the provided method.
648683
#
649684
# @api private
@@ -708,8 +743,10 @@ def apply_option(name)
708743
# @example Apply the inverse sorting params to the given block
709744
# context.with_inverse_sorting
710745
def with_inverse_sorting(opts = {})
746+
Mongoid::Warnings.warn_id_sort_deprecated if opts.try(:key?, :id_sort)
747+
711748
begin
712-
if sort = criteria.options[:sort] || ( { _id: 1 } unless opts[:id_sort] == :none )
749+
if sort = criteria.options[:sort] || ( { _id: 1 } unless opts.try(:fetch, :id_sort) == :none )
713750
@view = view.sort(Hash[sort.map{|k, v| [k, -1*v]}])
714751
end
715752
yield
@@ -917,6 +954,18 @@ def demongoize_with_field(field, value, is_translation)
917954
value.class.demongoize(value)
918955
end
919956
end
957+
958+
# Process the raw documents retrieved for #first/#last.
959+
#
960+
# @return [ Array<Document> | Document ] The list of documents or a
961+
# single document.
962+
def process_raw_docs(raw_docs, limit)
963+
docs = raw_docs.map do |d|
964+
Factory.from_db(klass, d, criteria)
965+
end
966+
docs = eager_load(docs)
967+
limit ? docs : docs.first
968+
end
920969
end
921970
end
922971
end

lib/mongoid/contextual/none.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,35 @@ def initialize(criteria)
117117
@criteria, @klass = criteria, criteria.klass
118118
end
119119

120+
# Always returns nil.
121+
#
122+
# @example Get the first document in null context.
123+
# context.first
124+
#
125+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
126+
# return, or a hash of options.
127+
#
128+
# @return [ nil ] Always nil.
129+
def first(limit_or_opts = nil)
130+
if !limit_or_opts.nil? && !limit_or_opts.is_a?(Hash)
131+
[]
132+
end
133+
end
134+
120135
# Always returns nil.
121136
#
122137
# @example Get the last document in null context.
123138
# context.last
124139
#
140+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
141+
# return, or a hash of options.
142+
#
125143
# @return [ nil ] Always nil.
126-
def last; nil; end
144+
def last(limit_or_opts = nil)
145+
if !limit_or_opts.nil? && !limit_or_opts.is_a?(Hash)
146+
[]
147+
end
148+
end
127149

128150
# Returns nil or empty array.
129151
#

lib/mongoid/findable.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,15 @@ def find_by!(attrs = {})
194194
# @example Find the first document.
195195
# Person.first
196196
#
197+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
198+
# return, or a hash of options.
199+
#
200+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
201+
# Don't apply a sort on _id if no other sort is defined on the criteria.
202+
#
197203
# @return [ Document ] The first matching document.
198-
def first
199-
with_default_scope.first
204+
def first(limit_or_opts = nil)
205+
with_default_scope.first(limit_or_opts)
200206
end
201207
alias :one :first
202208

@@ -205,9 +211,15 @@ def first
205211
# @example Find the last document.
206212
# Person.last
207213
#
214+
# @param [ Integer | Hash ] limit_or_opts The number of documents to
215+
# return, or a hash of options.
216+
#
217+
# @option limit_or_opts [ :none ] :id_sort This option is deprecated.
218+
# Don't apply a sort on _id if no other sort is defined on the criteria.
219+
#
208220
# @return [ Document ] The last matching document.
209-
def last
210-
with_default_scope.last
221+
def last(limit_or_opts = nil)
222+
with_default_scope.last(limit_or_opts)
211223
end
212224
end
213225
end

lib/mongoid/warnings.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
5+
# Encapsulates behavior around logging and caching warnings so they are only
6+
# logged once.
7+
#
8+
# @api private
9+
module Warnings
10+
11+
class << self
12+
def warning(id, message)
13+
singleton_class.class_eval do
14+
define_method("warn_#{id}") do
15+
unless instance_variable_get("@#{id}")
16+
Mongoid.logger.warn(message)
17+
instance_variable_set("@#{id}", true)
18+
end
19+
end
20+
end
21+
end
22+
end
23+
24+
warning :id_sort_deprecated, 'The :id_sort option has been deprecated. Use Mongo#take to get a document without a sort on _id.'
25+
end
26+
end
27+

0 commit comments

Comments
 (0)