Navigation

Queries

Queries

Mongoid provides a rich query DSL inspired by ActiveRecord. A trivial query looks as follows:

Band.where(name: "Depeche Mode")

A more complex query utilizing various Mongoid features could be as follows:

Band.
  where(:founded.gte => "1980-01-01").
  in(name: [ "Tool", "Deftones" ]).
  union.
  in(name: [ "Melvins" ])

The query methods return Mongoid::Criteria objects, which are chainable and lazily evaluated wrappers for MongoDB query language (MQL). The queries are executed when their result sets are iterated. For example:

# Construct a Criteria object:

Band.where(name: 'Deftones')
# => #<Mongoid::Criteria
#   selector: {"name"=>"Deftones"}
#   options:  {}
#   class:    Band
#   embedded: false>

# Evaluate the query and get matching documents:

Band.where(name: 'Deftones').to_a
# => [#<Band _id: 5ebdeddfe1b83265a376a760, name: "Deftones", description: nil>]

Methods like first and last return the individual documents immediately. Otherwise, iterating a Criteria object with methods like each or map retrieves the documents from the server. to_a can be used to force execution of a query that returns an array of documents, literally converting a Criteria object to an Array.

When a query method is called on a Criteria instance, the method returns a new Criteria instance with the new conditions added to the existing conditions:

scope = Band.where(:founded.gte => "1980-01-01")
# => #<Mongoid::Criteria
#   selector: {"founded"=>{"$gte"=>"1980-01-01"}}
#   options:  {}
#   class:    Band
#   embedded: false>

scope.where(:founded.lte => "2020-01-01")
# => #<Mongoid::Criteria
#   selector: {"founded"=>{"$gte"=>"1980-01-01", "$lte"=>"2020-01-01"}}
#   options:  {}
#   class:    Band
#   embedded: false>

scope
# => #<Mongoid::Criteria
#   selector: {"founded"=>{"$gte"=>"1980-01-01"}}
#   options:  {}
#   class:    Band
#   embedded: false>

Condition Syntax

Mongoid supports three ways of specifying individual conditions:

  1. Field syntax.
  2. MQL syntax.
  3. Symbol operator syntax.

All syntaxes support querying embedded documents using the dot notation. All syntaxes respect field types, if the field being queried is defined in the model class, and field aliases.

The examples in this section use the following model definition:

class Band
  include Mongoid::Document

  field :name, type: String
  field :founded, type: Integer
  field :m, as: :member_count, type: Integer

  embeds_one :manager
end

class Manager
  include Mongoid::Document

  embedded_in :band

  field :name, type: String
end

Field Syntax

The simplest querying syntax utilizes the basic Ruby hashes. Keys can be symbols or strings, and correspond to field names in MongoDB documents:

Band.where(name: "Depeche Mode")
#   => #<Mongoid::Criteria
#   selector: {"name"=>"Depeche Mode"}
#   options:  {}
#   class:    Band
#   embedded: false>

# Equivalent to:

Band.where("name" => "Depeche Mode")

MQL Syntax

An MQL operator may be specified on any field using the hash syntax:

Band.where(founded: {'$gt' => 1980})
# => #<Mongoid::Criteria
#   selector: {"founded"=>{"$gt"=>1980}}
#   options:  {}
#   class:    Band
#   embedded: false>

# Equivalent to:

Band.where('founded' => {'$gt' => 1980})

Symbol Operator Syntax

MQL operators may be specified as methods on symbols for the respective field name, as follows:

Band.where(:founded.gt => 1980)
# => #<Mongoid::Criteria
#   selector: {"founded"=>{"$gt"=>1980}}
#   options:  {}
#   class:    Band
#   embedded: false>

Embedded Documents

To match values of fields of embedded documents, use the dot notation:

Band.where('manager.name' => 'Smith')
# => #<Mongoid::Criteria
#   selector: {"manager.name"=>"Smith"}
#   options:  {}
#   class:    Band
#   embedded: false>

Band.where(:'manager.name'.ne => 'Smith')
# => #<Mongoid::Criteria
#   selector: {"manager.name"=>{"$ne"=>"Smith"}}
#   options:  {}
#   class:    Band
#   embedded: false>

Note

Queries always return top-level model instances, even if all of the conditions are referencing embedded documents.

Field Types

In order to query on a field, it is not necessary to add the field to the model class definition. However, if a field is defined in the model class, the type of the field is taken into account when constructing the query:

Band.where(name: 2020)
#   => #<Mongoid::Criteria
#   selector: {"name"=>"2020"}
#   options:  {}
#   class:    Band
#   embedded: false>

Band.where(founded: 2020)
# => #<Mongoid::Criteria
#   selector: {"founded"=>2020}
#   options:  {}
#   class:    Band
#   embedded: false>

Aliases

Queries take into account storage field names and field aliases:

Band.where(name: 'Astral Projection')
#   => #<Mongoid::Criteria
#   selector: {"n"=>"Astral Projection"}
#   options:  {}
#   class:    Band
#   embedded: false>

Since id and _id fields are aliases, either one can be used for queries:

Band.where(id: '5ebdeddfe1b83265a376a760')
# => #<Mongoid::Criteria
#   selector: {"_id"=>BSON::ObjectId('5ebdeddfe1b83265a376a760')}
#   options:  {}
#   class:    Band
#   embedded: false>

Logical Operations

Mongoid supports and, or, nor and not logical operations on Criteria objects. These methods take one or more hash of conditions or another Criteria object as their arguments, with not additionally having an argument-free version.

# and with conditions
Band.where(label: 'Trust in Trance').and(name: 'Astral Projection')

# or with scope
Band.where(label: 'Trust in Trance').or(Band.where(name: 'Astral Projection'))

# not with conditions
Band.not(label: 'Trust in Trance', name: 'Astral Projection')

# argument-less not
Band.not.where(label: 'Trust in Trance', name: 'Astral Projection')

For backwards compatibility with earlier Mongoid versions, all of the logical operation methods also accept arrays of parameters, which will be flattened to obtain the criteria. Passing arrays to logical operations is deprecated and may be removed in a future version of Mongoid.

The following calls all produce the same query conditions:

# Condition hashes passed to separate and invocations
Band.and(name: 'SUN Project').and(member_count: 2)

# Multiple condition hashes in the same and invocation
Band.and({name: 'SUN Project'}, {member_count: 2})

# Multiple condition hashes in an array - deprecated
Band.and([{name: 'SUN Project'}, {member_count: 2}])

# Condition hash in where and a scope
Band.where(name: 'SUN Project').and(Band.where(member_count: 2))

# Condition hash in and and a scope
Band.and({name: 'SUN Project'}, Band.where(member_count: 2))

# Scope as an array element, nested arrays - deprecated
Band.and([Band.where(name: 'SUN Project'), [{member_count: 2}]])

# All produce:
# => #<Mongoid::Criteria
#   selector: {"name"=>"SUN Project", "member_count"=>2}
#   options:  {}
#   class:    Band
#   embedded: false>

Operator Combinations

As of Mongoid 7.1, logical operators (and, or, nor and not) have been changed to have the the same semantics as those of ActiveRecord. To obtain the semantics of or as it behaved in Mongoid 7.0 and earlier, use any_of which is described below.

When conditions are specified on the same field multiple times, all conditions are added to the criteria:

Band.where(name: 1).where(name: 2).selector
# => {"name"=>"1", "$and"=>[{"name"=>"2"}]}

Band.where(name: 1).or(name: 2).selector
# => {"$or"=>[{"name"=>"1"}, {"name"=>"2"}]}

any_of, nor and not behave similarly, with not producing different query shapes as described below.

When and, or and nor logical operators are used, they operate on the criteria built up to that point and its argument. where has the same meaning as and:

# or joins the two conditions
Band.where(name: 'Sun').or(label: 'Trust').selector
# => {"$or"=>[{"name"=>"Sun"}, {"label"=>"Trust"}]}

# or applies only to the first condition, the second condition is added
# to the top level as $and
Band.or(name: 'Sun').where(label: 'Trust').selector
# => {"$or"=>[{"name"=>"Sun"}], "label"=>"Trust"}

# Same as previous example - where and and are aliases
Band.or(name: 'Sun').and(label: 'Trust').selector
# => {"$or"=>[{"name"=>"Sun"}], "label"=>"Trust"}

# Same operator can be stacked any number of times
Band.or(name: 'Sun').or(label: 'Trust').selector
# => {"$or"=>[{"name"=>"Sun"}, {"label"=>"Trust"}]}

# The label: Foo condition is added to the top level as $and
Band.where(name: 'Sun').or(label: 'Trust').where(label: 'Foo').selector
# => {"$or"=>[{"name"=>"Sun"}, {"label"=>"Trust"}], "label"=>"Foo"}

and Behavior

The and method will add new simple conditions to the top level of the criteria, unless the receiving criteria already has a condition on the respective fields, in which case the conditions will be combined with $and.

Band.where(label: 'Trust in Trance').and(name: 'Astral Projection').selector
# => {"label"=>"Trust in Trance Records", "name"=>"Astral Projection"}

Band.where(name: /Best/).and(name: 'Astral Projection').selector
# => {"name"=>/Best/, "$and"=>[{"name"=>"Astral Projection"}]}

As of Mongoid 7.1, specifying multiple criteria on the same field with and combines all criteria so specified, whereas in previous versions of Mongoid conditions on a field sometimes replaced previously specified conditions on the same field, depending on which form of and was used.

or/nor Behavior

or and nor produce $or and $nor MongoDB operators, respectively, using the receiver and all of the arguments as operands. For example:

Band.where(name: /Best/).or(name: 'Astral Projection')
# => {"$or"=>[{"name"=>/Best/}, {"name"=>"Astral Projection"}]}

Band.where(name: /Best/).and(name: 'Astral Projection').
  or(Band.where(label: /Records/)).and(label: 'Trust').selector
# => {"$or"=>[{"name"=>/Best/, "$and"=>[{"name"=>"Astral Projection"}]}, {"label"=>/Records/}], "label"=>"Trust"}

If the only condition on the receiver is another or/nor, the new conditions are added to the existing list:

Band.where(name: /Best/).or(name: 'Astral Projection').
  or(Band.where(label: /Records/)).selector
# => {"$or"=>[{"name"=>/Best/}, {"name"=>"Astral Projection"}, {"label"=>/Records/}]}

Use any_of to add a disjunction to a Criteria object while maintaining all of the conditions built up so far as they are.

any_of Behavior

any_of adds a disjunction built from its arguments to the existing conditions in the criteria. For example:

Band.where(label: /Trust/).any_of({name: 'Astral Projection'}, {name: /Best/})
# => {"label"=>/Trust/, "$or"=>[{"name"=>"Astral Projection"}, {"name"=>/Best/}]}

The conditions are hoisted to the top level if possible:

Band.where(label: /Trust/).any_of({name: 'Astral Projection'})
# => {"label"=>/Trust/, "name"=>"Astral Projection"}

not Behavior

not method can be called without arguments, in which case it will negate the next condition that is specified. not can also be called with one or more hash conditions or Criteria objects, which will all be negated and added to the criteria.

# not negates subsequent where
Band.not.where(name: 'Best').selector
# => {"name"=>{"$ne"=>"Best"}}

# The second where is added as $and
Band.not.where(name: 'Best').where(label: /Records/).selector
# => {"name"=>{"$ne"=>"Best"}, "label"=>/Records/}

# not negates its argument
Band.not(name: 'Best').selector
# => {"name"=>{"$ne"=>"Best"}}

Note

$not in MongoDB server cannot be used with a string argument. Mongoid uses $ne operator to achieve such a negation:

# String negation - uses $ne
Band.not.where(name: 'Best').selector
# => {"name"=>{"$ne"=>"Best"}}

# Regexp negation - uses $not
Band.not.where(name: /Best/).selector
# => {"name"=>{"$not"=>/Best/}}

Similarly to and, not will negate individual conditions for simple field criteria. For complex conditions and when a field already has a condition defined on it, since MongoDB server only supports the $not operator on a per-field basis rather than globally, Mongoid emulates $not by using an {'$and' => [{'$nor' => ...}]} construct:

# Simple condition
Band.not(name: /Best/).selector
# => {"name"=>{"$not"=>/Best/}}

# Complex conditions
Band.where(name: /Best/).not(name: 'Astral Projection').selector
# => {"name"=>/Best/, "$and"=>[{"$nor"=>[{"name"=>"Astral Projection"}]}]}

# Symbol operator syntax
Band.not(:name.ne => 'Astral Projection')
# => #<Mongoid::Criteria
#   selector: {"$and"=>[{"$nor"=>[{"name"=>{"$ne"=>"Astral Projection"}}]}]}
#   options:  {}
#   class:    Band
#   embedded: false>

If using not with arrays or regular expressions, please note the caveats/limitations of $not stated in the MongoDB server documentation.

Incremental Query Construction

By default, when conditions are added to a query, Mongoid considers each condition complete and independent from any other conditions potentially present in the query. For example, calling in twice adds two separate $in conditions:

Band.in(name: ['a']).in(name: ['b'])
=> #<Mongoid::Criteria
  selector: {"name"=>{"$in"=>["a"]}, "$and"=>[{"name"=>{"$in"=>["b"]}}]}
  options:  {}
  class:    Band
  embedded: false>

Some operator methods support building the condition incrementally. In this case, when an condition on a field which uses one of the supported operators is being added, if there already is a condition on the same field using the same operator, the operator expressions are combined according to the specified merge strategy.

Merge Strategies

Mongoid provides three merge strategies:

  • Override: the new operator instance replaces any existing conditions on the same field using the same operator.
  • Intersect: if there already is a condition using the same operator on the same field, the values of the existing condition are intersected with the values of the new condition and the result is stored as the operator value.
  • Union: if there already is a condition using the same operator on the same field, the values of the new condition are aded to the values of the exsting condition and the result is stored as the operator value.

The following snippet demonstrates all of the strategies, using in as the example operator:

Band.in(name: ['a']).override.in(name: ['b'])
=> #<Mongoid::Criteria
  selector: {"name"=>{"$in"=>["b"]}}
  options:  {}
  class:    Band
  embedded: false>

Band.in(name: ['a', 'b']).intersect.in(name: ['b', 'c'])
=> #<Mongoid::Criteria
  selector: {"name"=>{"$in"=>["b"]}}
  options:  {}
  class:    Band
  embedded: false>

Band.in(name: ['a']).union.in(name: ['b'])
=> #<Mongoid::Criteria
  selector: {"name"=>{"$in"=>["a", "b"]}}
  options:  {}
  class:    Band
  embedded: false>

The strategy is requested by calling override, intersect or union on a Criteria instance. The requested strategy applies to the next condition method called on the query. If the next condition method called does not support merge strategies, the strategy is reset, as shown in the following example:

Band.in(name: ['a']).union.ne(name: 'c').in(name: ['b'])
=> #<Mongoid::Criteria
  selector: {"name"=>{"$in"=>["a"], "$ne"=>"c"}, "$and"=>[{"name"=>{"$in"=>["b"]}}]}
  options:  {}
  class:    Band
  embedded: false>

Since ne does not support merge strategies, the union strategy was ignored and reset and when in was invoked the second time there was no strategy active.

Supported Operator Methods

The following operator methods support merge strategies:

  • all
  • in
  • nin

The set of methods may be expanded in future releases of Mongoid. For future compatibility, only invoke a strategy method when the next method call is an operator that supports merge strategies.

Note that the merge strategies are currently only applied when conditions are added through the designated methods. In the following example merge strategy is not applied because the second condition is added via where, not via in:

Band.in(foo: ['a']).union.where(foo: {'$in' => 'b'})
=> #<Mongoid::Criteria
  selector: {"foo"=>{"$in"=>["a"]}, "$and"=>[{"foo"=>{"$in"=>"b"}}]}
  options:  {}
  class:    Band
  embedded: false>

This behavior may change in a future release of Mongoid and should not be relied upon.

In contrast, it does not matter how the existing query was built when a merge strategy-supporting operator method is invoked. In the following example, the first condition was added through where but the strategy mechanism still applies:

Band.where(foo: {'$in' => ['a']}).union.in(foo: ['b'])
=> #<Mongoid::Criteria
  selector: {"foo"=>{"$in"=>["a", "b"]}}
  options:  {}
  class:    Band
  embedded: false>

Operator Value Expansion

Operator methods that support merge strategies all take Array as their value type. Mongoid expands Array-compatible types, such as a Range, when they are used with these operator methods:

Band.in(year: 1950..1960)
=> #<Mongoid::Criteria
  selector: {"year"=>{"$in"=>[1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960]}}
  options:  {}
  class:    Band
  embedded: false>

Additionally, Mongoid has historically wrapped non-Array values in arrays, as the following example demonstrates:

Band.in(year: 1950)
=> #<Mongoid::Criteria
  selector: {"year"=>{"$in"=>[1950]}}
  options:  {}
  class:    Band
  embedded: false>

This wrapping behavior is deprecated and should not be relied on. It may be removed in a future release of Mongoid.

Query Methods

elem_match

This matcher finds documents with array fields where one of the array values matches all of the conditions. For example:

class Band
  include Mongoid::Document
  field :name, type: String
  field :tours, type: Array
end

aerosmith = Band.create!(name: 'Aerosmith', tours: [
  {city: 'London', year: 1995},
  {city: 'New York', year: 1999},
])

Band.elem_match(tours: {city: 'London'}).to_a # => [aerosmith]

elem_match also works with embedded associations:

class Band
  include Mongoid::Document
  field :name, type: String
  embeds_many :tours
end

class Tour
  include Mongoid::Document
  field :city, type: String
  field :year, type: Integer
  embedded_in :band
end

dm = Band.create!(name: 'Depeche Mode')
aerosmith = Band.create!(name: 'Aerosmith')
Tour.create!(band: aerosmith, city: 'London', year: 1995)
Tour.create!(band: aerosmith, city: 'New York', year: 1999)

Band.elem_match(tours: {city: 'London'}).to_a # => [aerosmith]

elem_match does not work with non-embedded associations because MongoDB does not have joins - the conditions would be added to the collection that is the source of a non-embedded association rather than the collection of the association’s target.

elem_match can also be used with recursively embedded associations, as the following example shows:

class Tag
  include Mongoid::Document
  field :name, type: String
  recursively_embeds_many
end

root = Tag.create!(name: 'root')
sub1 = Tag.new(name: 'sub1', child_tags: [Tag.new(name: 'subsub1')])
root.child_tags << sub1
root.child_tags << Tag.new(name: 'sub2')
root.save!

Tag.elem_match(child_tags: {name: 'sub1'}).to_a # => [root]

root.child_tags.elem_match(child_tags: {name: 'subsub1'}).to_a # => [sub1]

Projection

only

The only method retrieves only the specified fields from the database. This operation is sometimes called “projection”.

class Band
  include Mongoid::Document

  field :name, type: String
  field :label, type: String

  embeds_many :tours
end

class Tour
  include Mongoid::Document

  field :city, type: String
  field :year, type: Integer

  embedded_in :band
end

band = Band.only(:name).first

Attempting to reference attributes which have not been loaded results in ActiveModel::MissingAttributeError.

band.label
# ActiveModel::MissingAttributeError (Missing attribute: 'label'.)

Even though Mongoid currently allows writing to attributes that have not been loaded, such writes will not be persisted (MONGOID-4701) and should therefore be avoided.

only can also be used with embedded associations:

band = Band.only(:name, 'tours.year').last
# => #<Band _id: 5c59afb1026d7c034dba46ac, name: "Aerosmith">

band.tours.first
# => #<Tour _id: 5c59afdf026d7c034dba46af, city: nil, year: 1995>

Note

Server versions 4.2 and lower allowed projecting both an association and the association’s fields in the same query, as follows:

band = Band.only(:tours, 'tours.year').last

The most recent projection specification overrided the earlier one. For example, the above query was equivalent to:

band = Band.only('tours.year').last

Server versions 4.4 and higher prohibit specifying an association and its fields in projection in the same query.

only can be specified with referenced associations (has_one, has_many, has_and_belongs_to_many) but is currently ignored for referenced associations - all fields of referenced associations will be loaded (MONGOID-4704).

Note that if a document has has_one or has_and_belongs_to_many associations, the fields with foreign keys must be included in the list of attributes loaded with only for those associations to be loaded. For example:

class Band
  include Mongoid::Document

  field :name, type: String

  has_and_belongs_to_many :managers
end

class Manager
  include Mongoid::Document

  has_and_belongs_to_many :bands
end

band = Band.create!(name: 'Astral Projection')
band.managers << Manager.new

Band.where(name: 'Astral Projection').only(:name).first.managers
# => []

Band.where(name: 'Astral Projection').only(:name, :manager_ids).first.managers
# => [#<Manager _id: 5c5dc2f0026d7c1730969843, band_ids: [BSON::ObjectId('5c5dc2f0026d7c1730969842')]>]

without

The opposite of only, without causes the specified fields to be omitted:

Band.without(:name)
# =>
# #<Mongoid::Criteria
#   selector: {}
#   options:  {:fields=>{"name"=>0}}
#   class:    Band
#   embedded: false>

Because Mongoid requires the _id field for various operations, it (as well as its id alias) cannot be omitted via without:

Band.without(:name, :id)
# =>
# #<Mongoid::Criteria
#   selector: {}
#   options:  {:fields=>{"name"=>0}}
#   class:    Band
#   embedded: false>

Band.without(:name, :_id)
# =>
# #<Mongoid::Criteria
#   selector: {}
#   options:  {:fields=>{"name"=>0}}
#   class:    Band
#   embedded: false>

Ordering

Mongoid provides the order method on Criteria objects and its alias, order_by, to specify the ordering of documents. These methods take a hash indicating which fields to order the documents by, and whether to use ascending or descending order for each field.

Band.order(name: 1)
# => #<Mongoid::Criteria
#   selector: {}
#   options:  {:sort=>{"name"=>1}}
#   class:    Band
#   embedded: false>

Band.order_by(name: -1, description: 1)
# => #<Mongoid::Criteria
#   selector: {}
#   options:  {:sort=>{"name"=>-1, "description"=>1}}
#   class:    Band
#   embedded: false>

Band.order_by(name: :desc, description: 'asc')
# => #<Mongoid::Criteria
#   selector: {}
#   options:  {:sort=>{"name"=>-1, "description"=>1}}
#   class:    Band
#   embedded: false>

The direction may be specified as integers 1 and -1 for ascending and descending, respectively, or as symbols :asc and :desc, or as strings "asc" and "desc".

Alternatively, order accepts an array of two-element arrays specifying the ordering. Field names and directions may be strings or symbols.

Band.order([['name', 'desc'], ['description', 'asc']])

Band.order([[:name, :desc], [:description, :asc]])

Another way of providing the order is to use #asc and #desc methods on symbols, as follows:

Band.order(:name.desc, :description.asc)

The arguments can be provided as a string using SQL syntax:

Band.order('name desc, description asc')

Finally, there are asc and desc methods that can be used instead of order/order_by:

Band.asc('name').desc('description')
# => #<Mongoid::Criteria
  selector: {}
  options:  {:sort=>{"name"=>1, "description"=>-1}}
  class:    Band
  embedded: false>

order calls can be chained, in which case the oldest calls define the most significant criteria and the newest calls define the least significant ones (since in Ruby hashes maintain the order of their keys):

Band.order('name desc').order('description asc')
# => #<Mongoid::Criteria
  selector: {}
  options:  {:sort=>{"name"=>-1, "description"=>1}}
  class:    Band
  embedded: false>

This can sometimes lead to surprising results if there are scopes, including the default scope, that use order/order_by. For example, in the following snippet bands are ordered by name first because the order in the default scope takes precedence over the order given in the query, due to the default scope being evaluated first:

class Band
  include Mongoid::Document

  field :name, type: String
  field :year, type: Integer

  default_scope -> { order(name: :asc) }
end

Band.order(year: :desc)
# => #<Mongoid::Criteria
  selector: {}
  options:  {:sort=>{"name"=>1, "year"=>-1}}
  class:    Band
  embedded: false>

Query Cache

When the query cache is enabled, Mongoid will cache results for MongoDB queries when the entire result set is returned in a single batch (1000 documents by default).

Each thread has its own query cache.

When the query cache is enabled, performing most write operations (insert, update, replace or delete) clears the cache of the thread issuing the write.

To enable the query cache manually for a code segment, use:

Mongoid::QueryCache.cache do
  # ...
end

The query cache can also be explicitly enabled and disabled, although it is recommended to use the block form described above:

Mongoid::QueryCache.enabled = true

# ...

Mongoid::QueryCache.enabled = false

Mongoid also provides a Rack middleware to enable the query cache automatically for each web request.

The Improved Driver Query Cache

The Mongoid query cache has been reimplemented in version 2.14.0 of the MongoDB Ruby Driver. The driver query cache is more correct and more effective, and it is recmomended that you upgrade to version 2.14.0 of the Ruby driver or newer.

For the purposes of this tutorial, the Mongoid query cache will be called “the legacy query cache”, and the driver query cache will be referred to as “the driver query cache.”

With driver versions 2.14.0 or newer, Mongoid will use the driver query cache instead of the legacy query cache. When used with older versions of the driver (that do not implement the query cache) Mongoid will fall back on the legacy query cache.

Mongoid will retain the interface (described above) for enabling and disabling the query cache. When using driver versions 2.14.0 or newer, this interface will affect the driver query cache.

Read more about the Ruby driver query cache in the driver documentation.

Warning

The legacy Mongoid query cache has been deprecated in favor of the query cache implemented in version 2.14.0 of the MongoDB Ruby driver. While the legacy query cache will continue to function, the driver query cache is more correct and more effective. If you plan on using the query cache, it is recommended that you upgrade to Ruby driver version 2.14.0 or newer.

Read more about the Ruby driver query cache in the driver documentation.

Legacy Query Cache Limitations

The following is a list of limitations of the legacy query cache:

  • The legacy query cache does not cache query results that exceed the batch size. The default batch size is 1000 documents.
  • The legacy query cache does not take into account read preference or read concern when deciding whether to return cached results. When performing the same query with a different read preference or read concern with the query cache enabled, incorrect results may be returned.
  • Bulk writes, as well as $out and $merge aggregation pipeline stages do not invalidate the query cache. Cached results may be stale after performing any of these operations.
  • Aggregation results are not cached.

A Note on using #first

Calling the first method on a model class imposes an ascending sort by the _id field on the underlying query. This may produce unexpected behavior with query caching.

For example, when calling all on a model class and then first, one would expect the second query to use the cached results from the first. However, because of the sort imposed on the second query, both methods will query the database and separately cache their results.

Band.all.to_a
#=> Queries the database and caches the results

Band.first
#=> Queries the database again because of the sort

To use the cached results, call all.to_a.first on the model class.

System Collections and the Query Cache

MongoDB stores system information in collections that use the database.system.* namespace pattern. These are called system collections.

Data in system collections can change due to activity not triggered by the application (such as internal server processes) and as a result of a variety of database commands issued by the application. Because of the difficulty of determining when the cached results for system collections should be expired, queries on system collections bypass the query cache.

Neither the legacy query cache nor the driver query cache will cache query results from system collections.

Band.all.to_a
#=> Queries the database and caches the results

Band.all.to_a.first
#=> Returns the first cached result

Finding By _id

Mongoid provides the find method on Criteria objects to find documents by their _id values:

Band.find('5f0e41d92c97a64a26aabd10')
# => #<Band _id: 5f0e41d92c97a64a26aabd10, name: "Juno Reactor">

The find method performs type conversion, if necessary, of the argument to the type declared in the model being queried for the _id field. By default, the _id type is BSON::ObjectId, thus the query above is equivalent to:

Band.find(BSON::ObjectId.from_string('5f0e41d92c97a64a26aabd10'))
# => #<Band _id: 5f0e41d92c97a64a26aabd10, name: "Juno Reactor">

Note

When querying collections directly using the driver, type conversion is not automatically performed:

Band.collection.find(_id: BSON::ObjectId.from_string('5f0e41d92c97a64a26aabd10')).first
# => {"_id"=>BSON::ObjectId('5f0e41d92c97a64a26aabd10'), "name"=>"Juno Reactor"}

Band.collection.find(_id: '5f0e41d92c97a64a26aabd10').first
# => nil

The find method can accept multiple arguments, or an array of arguments. In either case each of the arguments or array elements is taken to be an _id value, and documents with all of the specified _id values are returned in an array:

Band.find('5f0e41d92c97a64a26aabd10', '5f0e41b02c97a64a26aabd0e')
# => [#<Band _id: 5f0e41b02c97a64a26aabd0e, name: "SUN Project", description: nil, likes: nil>,
  #<Band _id: 5f0e41d92c97a64a26aabd10, name: "Juno Reactor", description: nil, likes: nil>]

Band.find(['5f0e41d92c97a64a26aabd10', '5f0e41b02c97a64a26aabd0e'])
# => [#<Band _id: 5f0e41b02c97a64a26aabd0e, name: "SUN Project", description: nil, likes: nil>,
  #<Band _id: 5f0e41d92c97a64a26aabd10, name: "Juno Reactor", description: nil, likes: nil>]

If the same _id value is given more than once, the corresponding document is only returned once:

Band.find('5f0e41b02c97a64a26aabd0e', '5f0e41b02c97a64a26aabd0e')
# => [#<Band _id: 5f0e41b02c97a64a26aabd0e, name: "SUN Project", description: nil, likes: nil>]

The documents returned are not ordered, and may be returned in a different order from the order of provided _id values, as illustrated in the above examples.

If any of the _id values are not found in the database, the behavior of find depends on the value of the raise_not_found_error configuration option. If the option is set to true, find raises Mongoid::Errors::DocumentNotFound if any of the _id``s are not found. If the option is set to ``false and find is given a single _id to find and there is no matching document, find returns nil. If the option is set to false and find is given an array of ``_id``s to find and some are not found, the return value is an array of documents that were found (which could be empty if no documents were found at all).

Additional Query Methods

Mongoid also has some helpful methods on criteria.

Operation Example

Criteria#count

Get the total number of documents matching a filter, or the total number of documents in a collection. Note this will always hit the database for the count.

As of Mongoid 7.2, the count method uses the count_documents driver helper to obtain the accurate count. previously the count driver helper was used which used collection metadata and was thus not necessarily accurate (but may have returned the result faster). Use estimated_count method to obtain an approximate number of documents in the collection quickly.

Band.count
Band.where(name: "Photek").count

Criteria#estimated_count

Get an approximate number of documents in the collection using the collection metadta. The estimated_count method does not accept query conditions; if any are given, it will raise Mongoid::Errors::InvalidEstimatedCountCriteria. If a model defines a default scope, estimated_count must be called on the unscoped model.

Band.count
Band.where(name: "Photek").count

class Contract
  include Mongoid::Document

  field :active, type: Boolean

  default_scope -> { where(active: true) }
end

Contract.estimated_count
# => raises Mongoid::Errors::InvalidEstimatedCountCriteria

Contract.unscoped.estimated_count
# => 0

Criteria#distinct

Get a list of distinct values for a single field. Note this will always hit the database for the distinct values.

Band.distinct(:name)
Band.where(:fans.gt => 100000).
  distinct(:name)

Criteria#each

Iterate over all matching documents in the criteria.

Band.where(members: 1).each do |band|
  p band.name
end

Criteria#exists?

Determine if any matching documents exist. Will return true if there are 1 or more.

Band.exists?
Band.where(name: "Photek").exists?

Criteria#find_by

Find a document by the provided attributes. If not found, raise an error or return nil depending on the value of the raise_not_found_error configuration option.

Band.find_by(name: "Photek")

Band.find_by(name: "Tool") do |band|
  band.impressions += 1
end

Criteria#find_or_create_by

Find a document by the provided attributes, and if not found create and return a newly persisted one. Note that attributes provided in the arguments to this method will override any set in ``create_with``.

Band.find_or_create_by(name: "Photek")
Band.where(:likes.gt => 10).find_or_create_by(name: "Photek")

find_or_create_by can be used on any scope, but in this case the criteria given by the scope and by find_or_create_by are combined. The following creates three bands:

Band.find_or_create_by(name: "Photek")
Band.where(name: "Photek").find_or_create_by(name: "Aerosmith")
# creates Aerosmith again because there is no band whose name
# is Photek and Aerosmith at the same time
Band.where(name: "Photek").find_or_create_by(name: "Aerosmith")

Criteria#find_or_initialize_by

Find a document by the provided attributes, and if not found return a new one.

Band.find_or_initialize_by(name: "Photek")
Band.where(:likes.gt => 10).find_or_initialize_by(name: "Photek")

Criteria#first|last

Finds a single document given the provided criteria. This automatically adds a sort on id. Opt out of adding the id sort with the {id_sort: :none} option.

Band.first
Band.where(:members.with_size => 3).first
Band.where(:members.with_size => 3).last

Criteria#first_or_create

Find the first document by the provided attributes, and if not found create and return a newly persisted one.

Band.where(name: "Photek").first_or_create

Criteria#first_or_create!

Find the first document by the provided attributes, and if not found create and return a newly persisted one using create!.

Band.where(name: "Photek").first_or_create!

Criteria#first_or_initialize

Find the first document by the provided attributes, and if not found return a new one.

Band.where(name: "Photek").first_or_initialize

Criteria#for_js

Find documents for a provided JavaScript expression, optionally with the specified variables added to the evaluation scope. The scope argument is supported in MongoDB 4.2 and lower. In MongoDB 3.6 and higher, prefer $expr over for_js.

# All MongoDB versions
Band.for_js("this.name = 'Tool'")

# MongoDB 4.2 and lower
Band.for_js("this.name = param", param: "Tool")

Criteria#length|size

Same as count but caches subsequent calls to the database

Band.length
Band.where(name: "FKA Twigs").size

Criteria#pluck

Get all the values for the provided field. Returns nil for unset fields and for non-existent fields.

Band.all.pluck(:name)

Eager Loading

Mongoid provides a facility to eager load documents from associations to prevent the n+1 issue when iterating over documents with association access. Eager loading is supported on all associations with the exception of polymorphic belongs_to associations.

class Band
  include Mongoid::Document
  has_many :albums
end

class Album
  include Mongoid::Document
  belongs_to :band
end

Band.includes(:albums).each do |band|
  p band.albums.first.name # Does not hit the database again.
end

Regular Expressions

MongoDB, and Mongoid, allow querying documents by regular expressions.

Given the following model definitions:

class Band
  include Mongoid::Document

  field :name, type: String
  field :description, type: String
end

Band.create!(name: 'Sun Project', description: "Sun\nProject")

… we can query using simple Ruby regular expressions in a natural way:

Band.where(name: /project/i).first
# => #<Band _id: 5dc9f7d5ce4ef34893354323, name: "Sun Project", description: "Sun\nProject">

It is also possible to query using PCRE syntax by constructing BSON::Regexp::Raw objects explicitly:

Band.where(description: /\AProject/).first
# => #<Band _id: 5dc9f7d5ce4ef34893354323, name: "Sun Project", description: "Sun\nProject">

Band.where(description: BSON::Regexp::Raw.new('^Project')).first
# => nil

Band.where(description: BSON::Regexp::Raw.new('^Project', 'm')).first
# => #<Band _id: 5dc9f7d5ce4ef34893354323, name: "Sun Project", description: "Sun\nProject">

Conditions On Fields

When a condition uses a field defined in the model, the value being specified in the condition is converted according to the rules of the field, if any. For example, consider the following model definition that contains a Time field, a Date field and an implicit Object field, and also intentionally does not define a field called deregistered_at:

class Voter
  include Mongoid::Document

  field :born_on, type: Date
  field :registered_at, type: Time
  field :voted_at
end

Queries on born_on and registered_at fields using Date and Time values, respectively, are straightforward:

Voter.where(born_on: Date.today).selector
# => {"born_on"=>2020-12-18 00:00:00 UTC}

Voter.where(registered_at: Time.now).selector
# => {"registered_at"=>2020-12-19 04:33:36.939788067 UTC}

But, note the differences in behavior when providing a Date instance in all possible scenarios:

Voter.where(born_on: Date.today).selector
# => {"born_on"=>2020-12-18 00:00:00 UTC}

Voter.where(registered_at: Date.today).selector
# => {"registered_at"=>2020-12-18 00:00:00 -0500}

Voter.where(voted_at: Date.today).selector
# => {"voted_at"=>Fri, 18 Dec 2020}

Voter.where(deregistered_at: Date.today).selector
# => {"deregistered_at"=>2020-12-18 00:00:00 UTC}

When using the registered_at field which is of type Time, the date was interpreted to be in local time (as per the configured time zone). When using the born_on field which is of type Date, the date was interpreted to be in UTC. When using the voted_at field which was defined without a type (hence implicitly as an Object), the date was used unmodified in the constructed query. When using a nonexistent field deregistered_at the date was interpreted to be in UTC and converted to a time, matching the behavior of querying a Date field.

Queries + Persistence

Mongoid supports persistence operations off of criteria in a light capacity for when you want to expressively perform multi document inserts, updates, and deletion.

Operation Example

Criteria#create

Create a newly persisted document.

Band.where(name: "Photek").create

Criteria#create!

Create a newly persisted document and raise an exception on validation failure.

Band.where(name: "Photek").create!

Criteria#build|new

Create a new (unsaved) document.

Band.where(name: "Photek").build
Band.where(name: "Photek").new

Criteria#update

Update attributes of the first matching document.

Band.where(name: "Photek").update(label: "Mute")

Criteria#update_all

Update attributes of all matching documents.

Band.where(members: 2).update_all(label: "Mute")

Criteria#add_to_set

Perform an $addToSet on all matching documents.

Band.where(name: "Photek").add_to_set(label: "Mute")

Criteria#bit

Perform a $bit on all matching documents.

Band.where(name: "Photek").bit(likes: { and: 14, or: 4 })

Criteria#inc

Perform an $inc on all matching documents.

Band.where(name: "Photek").inc(likes: 123)

Criteria#pop

Perform a $pop on all matching documents.

Band.where(name: "Photek").pop(members: -1)
Band.where(name: "Photek").pop(members: 1)

Criteria#pull

Perform a $pull on all matching documents.

Band.where(name: "Tool").pull(members: "Maynard")

Criteria#pull_all

Perform a $pullAll on all matching documents.

Band.where(name: "Tool").
  pull_all(:members, [ "Maynard", "Danny" ])

Criteria#push

Perform a $push on all matching documents.

Band.where(name: "Tool").push(members: "Maynard")

Criteria#push_all

Perform a $push with $each on all matching documents.

Band.where(name: "Tool").
  push_all(members: [ "Maynard", "Danny" ])

Criteria#rename

Perform a $rename on all matching documents.

Band.where(name: "Tool").rename(name: :title)

Criteria#set

Perform a $set on all matching documents.

Band.where(name: "Tool").set(likes: 10000)

Criteria#unset

Perform a $unset on all matching documents.

Band.where(name: "Tool").unset(:likes)

Criteria#delete

Deletes all matching documents in the database.

Band.where(label: "Mute").delete

Criteria#destroy

Deletes all matching documents in the database while running callbacks for all. This loads all documents into memory and can be an expensive operation.

Band.where(label: "Mute").destroy

Scoping

Scopes provide a convenient way to reuse common criteria with more business domain style syntax.

Named Scopes

Named scopes are simply criteria defined at class load that are referenced by a provided name. Just like normal criteria, they are lazy and chainable.

class Band
  include Mongoid::Document
  field :country, type: String
  field :genres, type: Array

  scope :english, ->{ where(country: "England") }
  scope :rock, ->{ where(:genres.in => [ "rock" ]) }
end

Band.english.rock # Get the English rock bands.

Named scopes can take procs and blocks for accepting parameters or extending functionality.

class Band
  include Mongoid::Document
  field :name, type: String
  field :country, type: String
  field :active, type: Boolean, default: true

  scope :named, ->(name){ where(name: name) }
  scope :active, ->{
    where(active: true) do
      def deutsch
        tap do |scope|
          scope.selector.store("origin" => "Deutschland")
        end
      end
    end
  }
end

Band.named("Depeche Mode") # Find Depeche Mode.
Band.active.deutsch # Find active German bands.

By default, Mongoid allows defining a scope that would shadow an existing class method, as the following example shows:

class Product
  include Mongoid::Document

  def self.fresh
    true
  end

  scope :fresh, ->{ where(fresh: true) }
end

To have Mongoid raise an error when a scope would overwrite an existing class method, set the scope_overwrite_exception configuration option to true.

Default Scopes

Default scopes can be useful when you find yourself applying the same criteria to most queries, and wish to specify these criteria as the default. Default scopes are procs that return criteria objects.

class Band
  include Mongoid::Document
  field :name, type: String
  field :active, type: Boolean

  default_scope ->{ where(active: true) }
end

Band.each do |band|
  # All bands here are active.
end

Specifying a default scope also initializes the fields of new models to the values given in the default scope, if the values are simple literals:

class Band
  include Mongoid::Document
  field :name, type: String
  field :active, type: Boolean
  field :num_tours, type: Integer

  default_scope ->{ where(active: true, num_tours: {'$gt' => 1}) }
end

# active is set, num_tours is not set
Band.new # => #<Band _id: 5c3f7452ce4ef378295ca5f5, name: nil, active: true, num_tours: nil>

Note that if a default value is provided both in the field definition and in the default scope, the value in the default scope takes precedence:

class Band
  include Mongoid::Document
  field :name, type: String
  field :active, type: Boolean, default: true

  default_scope ->{ where(active: false) }
end

Band.new # => #<Band _id: 5c3f74ddce4ef3791abbb088, name: nil, active: false>

Because a default scope initializes fields in new models as just described, defining a default scope with a dotted key and a simple literal value is not possible:

class Band
  include Mongoid::Document
  field :name, type: String
  field :tags, type: Hash

  default_scope ->{ where('tags.foo' => 'bar') }
end

Band.create! # exception: BSON::String::IllegalKey ('tags.foo' is an illegal key in MongoDB. Keys may not start with '$' or contain a '.'.)

A workaround is to define the default scope as a complex query:

class Band
  include Mongoid::Document
  field :name, type: String
  field :tags, type: Hash

  default_scope ->{ where('tags.foo' => {'$eq' => 'bar'}) }
end

Band.create!(tags: {hello: 'world'})
Band.create!(tags: {foo: 'bar'})
Band.count # => 1

You can tell Mongoid not to apply the default scope by using unscoped, which can be inline or take a block.

Band.unscoped.where(name: "Depeche Mode")
Band.unscoped do
  Band.where(name: "Depeche Mode")
end

You can also tell Mongoid to explicitly apply the default scope again later to always ensure it’s there.

Band.unscoped.where(name: "Depeche Mode").scoped

If you are using a default scope on a model that is part of an association, you must reload the association to have scoping reapplied. This is important to note if you change a value of a document in the association that would affect its visibility within the scoped association.

class Label
  include Mongoid::Document
  embeds_many :bands
end

class Band
  include Mongoid::Document
  field :active, default: true
  embedded_in :label
  default_scope ->{ where(active: true) }
end

label.bands.push(band)
label.bands # [ band ]
band.update_attribute(:active, false)
label.bands # [ band ] Must reload.
label.reload.bands # []

Class Methods

Class methods on models that return criteria objects are also treated like scopes, and can be chained as well.

class Band
  include Mongoid::Document
  field :name, type: String
  field :active, type: Boolean, default: true

  def self.active
    where(active: true)
  end
end

Band.active

Map/Reduce

Mongoid provides a DSL around MongoDB’s map/reduce framework, for performing custom map/reduce jobs or simple aggregations.

Execution

You can tell Mongoid off the class or a criteria to perform a map/reduce by calling map_reduce and providing map and reduce javascript functions.

map = %Q{
  function() {
    emit(this.name, { likes: this.likes });
  }
}

reduce = %Q{
  function(key, values) {
    var result = { likes: 0 };
    values.forEach(function(value) {
      result.likes += value.likes;
    });
    return result;
  }
}

Band.where(:likes.gt => 100).map_reduce(map, reduce).out(inline: 1)

Just like criteria, map/reduce calls are lazily evaluated. So nothing will hit the database until you iterate over the results, or make a call on the wrapper that would need to force a database hit.

Band.map_reduce(map, reduce).out(replace: "mr-results").each do |document|
  p document # { "_id" => "Tool", "value" => { "likes" => 200 }}
end

The only required thing you provide along with a map/reduce is where to output the results. If you do not provide this an error will be raised. Valid options to #out are:

  • inline: 1: Don’t store the output in a collection.
  • replace: "name": Store in a collection with the provided name, and overwrite any documents that exist in it.
  • merge: "name": Store in a collection with the provided name, and merge the results with the existing documents.
  • reduce: "name": Store in a collection with the provided name, and reduce all existing results in that collection.

Raw Results

Results of Map/Reduce execution can be retrieved via the execute method or its aliases raw and results:

mr = Band.where(:likes.gt => 100).map_reduce(map, reduce).out(inline: 1)

mr.execute
# => {"results"=>[{"_id"=>"Tool", "value"=>{"likes"=>200.0}}],
      "timeMillis"=>14,
      "counts"=>{"input"=>4, "emit"=>4, "reduce"=>1, "output"=>1},
      "ok"=>1.0,
      "$clusterTime"=>{"clusterTime"=>#<BSON::Timestamp:0x00005633c2c2ad20 @seconds=1590105400, @increment=1>, "signature"=>{"hash"=><BSON::Binary:0x12240 type=generic data=0x0000000000000000...>, "keyId"=>0}},
      "operationTime"=>#<BSON::Timestamp:0x00005633c2c2aaf0 @seconds=1590105400, @increment=1>}

Statistics

MongoDB servers 4.2 and lower provide Map/Reduce execution statistics. As of MongoDB 4.4, Map/Reduce is implemented via the aggregation pipeline and statistics described in this section are not available.

The following methods are provided on the MapReduce object:

  • counts: Number of documents read, emitted, reduced and output through the pipeline.
  • input, emitted, reduced, output: individual count methods. Note that emitted and reduced methods are named differently from hash keys in counts.
  • time: The time, in milliseconds, that Map/Reduce pipeline took to execute.

The following code illustrates retrieving the statistics:

mr = Band.where(:likes.gt => 100).map_reduce(map, reduce).out(inline: 1)

mr.counts
# => {"input"=>4, "emit"=>4, "reduce"=>1, "output"=>1}

mr.input
# => 4

mr.emitted
# => 4

mr.reduced
# => 1

mr.output
# => 1

mr.time
# => 14

Note

Each statistics method invocation re-executes the Map/Reduce pipeline. The results of execution are not stored by Mongoid. Consider using the execute method to retrieve the raw results and obtaining the statistics from the raw results if multiple statistics are desired.

←   Persistence Associations  →