Mongoid 8.0#

This page describes significant changes and improvements in Mongoid 8.0. The complete list of releases is available on GitHub and in JIRA; please consult GitHub releases for detailed release notes and JIRA for the complete list of issues fixed in each release, including bug fixes.

Support for MongoDB 3.4 and Earlier Servers Dropped#

Mongoid 8 requires MongoDB 3.6 or newer. Earlier server versions are not supported.

Support for Ruby 2.5 Dropped#

Mongoid 8 requires Ruby 2.6 or newer. Earlier Ruby versions are not supported.

Support for Rails 5.1 Dropped#

Mongoid 8 requires Rails 5.2 or newer. Earlier Rails versions are not supported.

Default Option Values Changed#

Breaking change: The following options have had their default values changed in Mongoid 8.0:

  • :broken_aggregables => false

  • :broken_alias_handling => false

  • :broken_and => false

  • :broken_scoping => false

  • :broken_updates => false

  • :compare_time_by_ms => true

  • :legacy_attributes => false

  • :legacy_pluck_distinct => false

  • :legacy_triple_equals => false

  • :object_id_as_json_oid => false

  • :overwrite_chained_operators => false

Please refer to configuration option for the description and effects of each of these options.

Decimal128-backed BigDecimal Fields#

Mongoid 8 introduces the map_big_decimal_to_decimal128 feature flag, which allows values assigned to a field of type BigDecimal to be stored as type String in the database for compatibility with Mongoid 7 and earlier. In Mongoid 8 by default (with this feature flag turned on), values assigned to fields of type BigDecimal are stored in the database as type BSON::Decimal128. In Mongoid 7 and earlier, and in Mongoid 8 with this feature flag turned off, values assigned to fields of type BigDecimal are stored as Strings. See the section on BigDecimal Fields for more details.

Storing/Retrieving/Evolving Uncastable Values#

Breaking change: In Mongoid 8, Mongoid standardizes the storing, retrieving and evolving of “uncastable values.” On attempting to read or write an uncastable value, a nil is returned or written instead. When attempting to evolve an uncastable value, the inputted value is returned. See the section on Uncastable Values for more details.

Some mongoize, demongoize and evolve methods were also changed to perform consistently with rails and the other mongoize, demongoize and evolve methods. The following is a table of the changes in functionality:

Field Type

Situation

Previous Functionality

New Functionality

Boolean

When a non-boolean string is assigned: “bogus value”

return false

return nil

Array/Hash

When a value that is not an array or hash is assigned

raise InvalidValue error

return nil

Set

When a value that is not a set is assigned: 1

raise NoMethodError Exception: undefined method to_a for 1:Integer

return nil

Regexp

When persisting and reading a Regexp from the database

return a BSON::Regexp::Raw

return a Regexp

Time/DateTime

When assigning a bogus value: :bogus

raise NoMethodError Exception: undefined method to_i for :bogus:Symbol

return nil

Time/DateTime

When demongoizing a non-Time value: “bogus”, Date.today

raise NoMethodError Exception: undefined method getlocal for “bogus”:String

“bogus”: return nil

Date.today: return a Time/DateTime

Date

When assigning or demongoizing a bogus value: :bogus

raise NoMethodError Exception: undefined method year for :bogus:Symbol

return nil

Time/DateTime /Date

When demongoizing a valid string: “2022-07-14 14:45:51 -0400”

raise NoMethodError Exception: undefined method getlocal for “2022-07-14 14:45:51 -0400”:String

return a Time/DateTime/Date

All Types

When an uncastable value is assigned or demongoized

undefined behavior, occasionally raise NoMethodError

return nil

All Types

When an uncastable value is evolved

undefined behavior, occasionally raise NoMethodError

return inputted value

Note

The demongoize methods on container objects (i.e. Hash, Array) have not changed to prevent bugs when modifying and saving those objects. See https://jira.mongodb.org/browse/MONGOID-2951 for a longer discussion on these bugs.

Changes to the attributes_before_type_cast Hash#

The attributes_before_type_cast hash has been changed to function more like ActiveRecord:

  • On instantiation of a new model (without parameters), the attributes_before_type_cast hash has the same contents as the attributes hash. If parameters are passed to the initializer, those values will be stored in the attributes_before_type_cast hash before they are mongoized.

  • When assigning a value to the model, the mongoized value (i.e. when assiging ‘1’ to an Integer field, it is mongoized to 1) is stored in the attributes hash, whereas the raw value (i.e. ‘1’) is stored in the attributes_before_type_cast hash.

  • When saving, creating (i.e. using the create! method), or reloading the model, the attributes_before_type_cast hash is reset to have the same contents as the attributes hash.

  • When reading a document from the database, the attributes_before_type_cast hash contains the attributes as they appear in the database, as opposed to their demongoized form.

Order of Callback Invocation#

Breaking change: Mongoid 8.0 changes the order of _create and _save callback invocation for documents with associations.

Referenced associations (has_one and has_many):

Mongoid 8.0

Mongoid 7

Parent :before_save

Parent :before_save

Parent :around_save_open

Parent :around_save_open

Parent :before_create

Parent :before_create

Parent :around_create_open

Parent :around_create_open

Parent persisted in MongoDB

Parent persisted in MongoDB

Child :before_save

Parent :around_create_close

Child :around_save_open

Parent :after_create

Child :before_create

Child :before_save

Child :around_create_open

Child :around_save_open

Child :before_create

Child :around_create_open

Child persisted in MongoDB

Child persisted in MongoDB

Child :around_create_close

Child :around_create_close

Child :after_create

Child :after_create

Child :around_save_close

Child :around_save_close

Child :after_save

Child :after_save

Parent :around_create_close

Parent :around_save_close

Parent :after_create

Parent :after_save

Parent :around_save_close

Parent :after_save

Embedded associations (embeds_one and embeds_many):

Mongoid 8.0

Mongoid 7

Parent :before_save

Child :before_save

Parent :around_save_open

Child :around_save_open

Parent :before_create

Child :around_save_close

Parent :around_create_open

Child :after_save

Child :before_save

Parent :before_save

Child :around_save_open

Parent :around_save_open

Child :before_create

Child :before_create

Child :around_create_open

Child :around_create_open

Child :around_create_close

Child :after_create

Parent :before_create

Parent :around_create_open

Document persisted in MongoDB

Document persisted in MongoDB

Child :around_create_close

Child :after_create

Child :around_save_close

Child :after_save

Parent :around_create_close

Parent :around_create_close

Parent :after_create

Parent :after_create

Parent :around_save_close

Parent :around_save_close

Parent :after_save

Parent :after_save

Changeable Module Behavior Made Compatible With ActiveModel::Dirty#

When updating documents, it is now possible to get updated attribute values in after_* callbacks. This follows ActiveRecord/ActiveModel behavior.

class Cat
  include Mongoid::Document

  field :age, type: Integer

  after_save do
    p self
    p attribute_was(:age)
  end
end

a = Cat.create!
a.age = 2
a.save!

Mongoid 8.0 output:

#<Cat _id: 60aef1652c97a617438dc9bb, age: 2>
2

Mongoid 7 output:

#<Cat _id: 60aef1652c97a617438dc9bb, age: 2>
nil

Notice that in 7 attribute_was(:age) returns the old attribute value, while in 8.0 attribute_was(:age) returns the new value.

*_previously_was, previously_new_record?, and previously_persisted? helpers#

Mongoid 8.0 introduces ActiveModel-compatible *_previously_was helpers, as well as ActiveRecord-compatible previously_new_record? and previously_persisted? helpers:

class User
  include Mongoid::Document

  field :name, type: String
  field :age, type: Integer
end

user = User.create!(name: 'Sam', age: 18)
user.previously_new_record?     # => true

user.name = "Nick"
user.save!
user.name_previously_was        # => "Sam"
user.age_previously_was         # => 18
user.previously_new_record?     # => false

user.destroy
user.previously_persisted?   # => true

Unknown Field Type Symbols/Strings Prohibited#

Breaking change: Mongoid 8 prohibits using symbols and strings as field types when these symbols and strings do not map to a known type. Previously such usage would create a field of type Object.

Mongoid 8 behavior:

class User
  include Mongoid::Document

  field :name, type: :bogus
  # => raises Mongoid::Errors::InvalidFieldType
end

Mongoid 7 behavior:

class User
  include Mongoid::Document

  field :name, type: :bogus
  # Equivalent to:
  field :name
end

any_of Adds Multiple Arguments As Top-Level Conditions#

Breaking change: When any_of is invoked with multiple conditions, the conditions are now added to the top level of the criteria, same as when any_of is invoked with a single condition. Previously when multiple conditions were provided, and the criteria already had an $or operator, the new conditions would be added to the existing $or as an additional branch.

Mongoid 8.0 behavior:

Band.any_of({name: 'The Rolling Stones'}, {founded: 1990}).
  any_of({members: 2}, {last_tour: 1995})
# =>
# #<Mongoid::Criteria
#   selector: {"$or"=>[{"name"=>"The Rolling Stones"}, {"founded"=>1990}],
#     "$and"=>[{"$or"=>[{"members"=>2}, {"last_tour"=>1995}]}]}
#   options:  {}
#   class:    Band
#   embedded: false>

Band.any_of({name: 'The Rolling Stones'}, {founded: 1990}).any_of({members: 2})
# =>
# #<Mongoid::Criteria
#   selector: {"$or"=>[{"name"=>"The Rolling Stones"}, {"founded"=>1990}], "members"=>2}
#   options:  {}
#   class:    Band
#   embedded: false>

Mongoid 7 behavior:

Band.any_of({name: 'The Rolling Stones'}, {founded: 1990}).
  any_of({members: 2}, {last_tour: 1995})
# =>
# #<Mongoid::Criteria
#   selector: {"$or"=>[{"name"=>"The Rolling Stones"}, {"founded"=>1990},
#     {"members"=>2}, {"last_tour"=>1995}]}
#   options:  {}
#   class:    Band
#   embedded: false>

Band.any_of({name: 'The Rolling Stones'}, {founded: 1990}).any_of({members: 2})
# =>
# #<Mongoid::Criteria
#   selector: {"$or"=>[{"name"=>"The Rolling Stones"}, {"founded"=>1990}], "members"=>2}
#   options:  {}
#   class:    Band
#   embedded: false>

#pluck on Embedded Criteria Returns nil Values#

Mongoid 8 fixes a bug where calling #pluck on a Mongoid::Criteria for embedded documents discarded nil values. This behavior was inconsistent with both the #pluck method in ActiveSupport and with how #pluck works when reading documents from the database.

Mongoid 8.0 behavior:

class Address
  include Mongoid::Document

  embedded_in :mall

  field :street, type: String
end

class Mall
  include Mongoid::Document

  embeds_many :addresses
end

mall = Mall.create!
mall.addresses.create!(street: "Elm Street")
mall.addresses.create!(street: nil)

# Pluck from embedded document criteria
mall.addresses.all.pluck(:street)
  #=> ['Elm Street', nil]

Mongoid 7 behavior, given the same setup:

# Pluck from embedded document criteria
mall.addresses.all.pluck(:street)
  #=> ['Elm Street']

For clarity, the following behavior is unchanged from Mongoid 7 to Mongoid 8.0:

# Pluck from database
Mall.all.pluck('addresses.street')
  #=> [ ['Elm Street', nil] ]

# Pluck using ActiveSupport Array#pluck
mall.addresses.pluck(:street)
  #=> ['Elm Street', nil]

Replaced Mongoid::Criteria#geo_spacial with #geo_spatial#

The previously deprecated Mongoid::Criteria#geo_spacial method has been removed in Mongoid 8. It has been replaced one-for-one with #geo_spatial which was added in Mongoid 7.2.0.

Implemented .tally method on Mongoid#Criteria#

Mongoid 8 implements the .tally method on Mongoid#Criteria. tally takes a field name as a parameter and returns a mapping from values to their counts. For example, take the following model:

class User
  include Mongoid::Document
  field :age
end

and the following documents in the database:

{ _id: 1, age: 21 }
{ _id: 2, age: 21 }
{ _id: 3, age: 22 }

Calling tally on the age field yields the following:

User.tally("age")
# => { 21 => 2, 22 => 1 }

The tally method accepts the dot notation and field aliases. It also allows for tallying localized fields.

Implemented .pick method on Mongoid#Criteria#

Mongoid 8 implements the .pick method on Mongoid#Criteria. pick takes one or more field names as a parameter and returns the values for those fields from one document. Consider the following model:

class User
  include Mongoid::Document
  field :age
end

and the following documents in the database:

{ _id: 1, age: 21 }
{ _id: 2, age: 21 }
{ _id: 3, age: 22 }

Calling pick on the age field yields the following:

User.pick(:age)
# => 21

This method does not apply a sort to the documents, so it will not necessarily return the values from the first document.

The pick method accepts the dot notation and field aliases. It also allows for picking localized fields.

find delegates to Enumerable#find when given a block#

When given a block, without _id arguments, find delegates to Enumerable#find. Consider the following model:

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

Band.create!(name: "Depeche Mode")
Band.create!(name: "The Rolling Stones")

Calling find with a block returns the first document for which the block returns true:

Band.find do |b|
  b.name == "Depeche Mode"
end
# => #<Band _id: 62c58e383282a4cbe82bd74b, name: "Depeche Mode">

No Longer Persisting Documents with Invalid belongs_to Associations#

In Mongoid 8, if an invalid document is assigned to a belongs_to association, and the base document is saved, if the belongs_to association had the option optional: false or optional is unset and the global flag belongs_to_required_by_default is true, neither the document nor the associated document will be persisted. For example, given the following models:

class Parent
  include Mongoid::Document
  has_one :child
  field :name
  validates :name, presence: true
end

class Child
  include Mongoid::Document

  belongs_to :parent, autosave: true, validate: false, optional: false
end

child = Child.new
parent = Parent.new
child.parent = parent # parent is invalid, it does not have a name
child.save

In this case, both the child and the parent will not be persisted.

Note

If save! were called, a validation error would be raised.

If optional is false, then the Child will be persisted with a parent_id, but the parent will not be persisted:

child = Child.new
parent = Parent.new
child.parent = parent # parent is invalid, it does not have a name
child.save

p Child.first
# => <Child _id: 629a50b0d1327aad89d214d2, parent_id: BSON::ObjectId('629a50b0d1327aad89d214d3')>
p Parent.first
# => nil

If you want the functionality of neither document being persisted in Mongoid 7 or 8 and earlier, the validate: true option can be set on the association. If you want the functionality of only the Child being persisted, the validate: false option can be set on the association.

Update Local HABTM Association Correctly#

In Mongoid 8, when pushing persisted elements to a HABTM association, the association will now update correctly without requiring a reload. For example:

class User
  include Mongoid::Document
  has_and_belongs_to_many :posts
end

class Post
  include Mongoid::Document
  has_and_belongs_to_many :users
end

user1 = User.create!
user2 = User.create!

post = user1.posts.create!

p post.users.length
# => 1

post.users << user2

p post.users.length
# => 1 in Mongoid 7, 2 in Mongoid 8

p post.reload.users.length
# => 2

As you can see from this example, after pushing user2 to the users array, Mongoid 8 correctly updates the number of elements without requiring a reload.

Repaired Storing Strings in BSON::Binary fields#

Breaking change: In Mongoid 8, storing a String in a field of type BSON::Binary will be stored in and returned from the database as a BSON::Binary. Prior to Mongoid 8 it was stored and returned as a String:

class Registry
  include Mongoid::Document
  field :data, type: BSON::Binary
end

registry = Registry.create!(data: "data!")
p registry.data
# => Mongoid 7: "data!"
# => Mongoid 8: <BSON::Binary:0x2580 type=generic data=0x6461746121...>

registry = Registry.find(registry.id)
p registry.data
# => Mongoid 7: "data!"
# => Mongoid 8: <BSON::Binary:0x2600 type=generic data=0x6461746121...>

Removed Document#to_a Method#

The previously deprecated Document#to_a method has been removed in Mongoid 8.

Removed :drop_dups Option from Indexes#

The :drop_dups option has been removed from the index macro. This option was specific to MongoDB server 2.6 and earlier, which Mongoid no longer supports.

Removed Mongoid::Errors::EagerLoad Exception Class#

The previously deprecated Mongoid::Errors::EagerLoad exception class has been removed in Mongoid 8. It has not been used by Mongoid since version 7.1.1 when eager loading for polymorphic belongs_to associations was implemented.

Removed Deprecated Constants#

Mongoid 8 removes the following deprecated constants that are not expected to have been used outside of Mongoid:

  • Mongoid::Extensions::Date::EPOCH

  • Mongoid::Extensions::Time::EPOCH

  • Mongoid::Factory::TYPE

Removed Array#update_values and Hash#update_values methods#

The previously deprecated Array#update_values and Hash#update_values methods have been removed in Mongoid 8.

Deprecated the geoHaystack, geoSearch Options#

The geoHaystack and geoSearch options on indexes have been deprecated.

:id_sort Option on #first/last Removed#

Support for the :id_sort option on the #first and #last options has been dropped. These methods now only except a limit as a positional argument.

Mongoid::Criteria Cache Removed#

Support for individually caching criteria objects has been dropped in Mongoid 8.

In order to get caching functionality, enable the Mongoid Query Cache. See the section on Query Cache for more details.