diff --git a/.gitignore b/.gitignore
index b3dfb453e..a96439c8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ examples
.env
.env.private*
.tool-versions
+build
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index a32c30be4..d9250b2ee 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,12 +1,12 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 1000`
-# on 2023-04-20 23:45:43 UTC using RuboCop version 1.50.0.
+# on 2023-08-26 19:42:04 UTC using RuboCop version 1.49.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 121
+# Offense count: 108
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 154
@@ -25,14 +25,14 @@ Metrics/BlockNesting:
# Offense count: 17
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
- Max: 329
+ Max: 327
-# Offense count: 54
+# Offense count: 50
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 33
-# Offense count: 186
+# Offense count: 182
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Max: 87
@@ -48,21 +48,20 @@ Metrics/ParameterLists:
MaxOptionalParameters: 4
Max: 6
-# Offense count: 45
+# Offense count: 40
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 20
-# Offense count: 7
+# Offense count: 3
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName:
Exclude:
- 'lib/mongoid/association/relatable.rb'
- 'lib/mongoid/document.rb'
- - 'lib/mongoid/traversable.rb'
-# Offense count: 25
+# Offense count: 27
RSpec/AnyInstance:
Exclude:
- 'spec/mongoid/association/referenced/belongs_to/buildable_spec.rb'
@@ -81,7 +80,7 @@ RSpec/AnyInstance:
RSpec/BeforeAfterAll:
Enabled: false
-# Offense count: 832
+# Offense count: 833
# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
@@ -204,10 +203,11 @@ RSpec/ContextWording:
- 'spec/support/immutable_ids.rb'
- 'spec/support/shared/time.rb'
-# Offense count: 57
+# Offense count: 58
# Configuration parameters: IgnoredMetadata.
RSpec/DescribeClass:
Exclude:
+ - 'spec/integration/active_job_spec.rb'
- 'spec/integration/app_spec.rb'
- 'spec/integration/associations/belongs_to_spec.rb'
- 'spec/integration/associations/embedded_dirty_spec.rb'
@@ -248,7 +248,7 @@ RSpec/DescribeClass:
- 'spec/mongoid/railties/console_sandbox_spec.rb'
- 'spec/mongoid/tasks/database_rake_spec.rb'
-# Offense count: 133
+# Offense count: 126
RSpec/ExpectInHook:
Exclude:
- 'spec/integration/associations/embedded_spec.rb'
@@ -264,7 +264,6 @@ RSpec/ExpectInHook:
- 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_persistence_spec.rb'
- 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_many/binding_spec.rb'
- - 'spec/mongoid/association/referenced/has_many/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_one/binding_spec.rb'
- 'spec/mongoid/association/referenced/has_one/buildable_spec.rb'
- 'spec/mongoid/association/referenced/has_one/proxy_spec.rb'
@@ -292,7 +291,7 @@ RSpec/ExpectInHook:
- 'spec/mongoid/validatable/presence_spec.rb'
- 'spec/support/immutable_ids.rb'
-# Offense count: 24
+# Offense count: 25
# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
# Include: **/*_spec*rb*, **/spec/**/*
RSpec/FilePath:
@@ -314,6 +313,7 @@ RSpec/FilePath:
- 'spec/mongoid/extensions/raw_value_spec.rb'
- 'spec/mongoid/extensions/stringified_symbol_spec.rb'
- 'spec/mongoid/loading_spec.rb'
+ - 'spec/mongoid/railties/bson_object_id_serializer_spec.rb'
- 'spec/mongoid/validatable/associated_spec.rb'
- 'spec/mongoid/validatable/format_spec.rb'
- 'spec/mongoid/validatable/length_spec.rb'
@@ -322,7 +322,7 @@ RSpec/FilePath:
- 'spec/mongoid/validatable/uniqueness_spec.rb'
- 'spec/mongoid/version_spec.rb'
-# Offense count: 17
+# Offense count: 18
RSpec/IteratedExpectation:
Exclude:
- 'spec/mongoid/association/referenced/belongs_to/eager_spec.rb'
@@ -333,10 +333,12 @@ RSpec/IteratedExpectation:
- 'spec/mongoid/contextual/mongo_spec.rb'
- 'spec/mongoid/criteria_spec.rb'
- 'spec/mongoid/findable_spec.rb'
+ - 'spec/mongoid_spec.rb'
-# Offense count: 229
+# Offense count: 230
RSpec/LeakyConstantDeclaration:
Exclude:
+ - 'spec/integration/active_job_spec.rb'
- 'spec/integration/app_spec.rb'
- 'spec/integration/callbacks_spec.rb'
- 'spec/integration/discriminator_key_spec.rb'
@@ -392,7 +394,7 @@ RSpec/LeakyConstantDeclaration:
- 'spec/mongoid/validatable/numericality_spec.rb'
- 'spec/mongoid/validatable/uniqueness_spec.rb'
-# Offense count: 523
+# Offense count: 499
RSpec/LetSetup:
Exclude:
- 'spec/integration/matcher_examples_spec.rb'
@@ -416,7 +418,6 @@ RSpec/LetSetup:
- 'spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb'
- 'spec/mongoid/association/referenced/has_many/eager_spec.rb'
- 'spec/mongoid/association/referenced/has_many/enumerable_spec.rb'
- - 'spec/mongoid/association/referenced/has_many/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_many_spec.rb'
- 'spec/mongoid/association/referenced/has_one/buildable_spec.rb'
- 'spec/mongoid/association/referenced/has_one/eager_spec.rb'
@@ -467,8 +468,8 @@ RSpec/LetSetup:
- 'spec/mongoid/touchable_spec.rb'
- 'spec/mongoid/validatable/uniqueness_spec.rb'
-# Offense count: 302
-# Configuration parameters: .
+# Offense count: 298
+# Configuration parameters: EnforcedStyle.
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
EnforcedStyle: receive
@@ -497,12 +498,12 @@ RSpec/NamedSubject:
- 'spec/mongoid/extensions/raw_value_spec.rb'
- 'spec/mongoid/matcher/expression_spec.rb'
-# Offense count: 5010
+# Offense count: 5025
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 13
-# Offense count: 33
+# Offense count: 37
# Configuration parameters: AllowedPatterns.
# AllowedPatterns: ^expect_, ^assert_
RSpec/NoExpectationExample:
@@ -515,6 +516,7 @@ RSpec/NoExpectationExample:
- 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb'
- 'spec/mongoid/attributes_spec.rb'
- 'spec/mongoid/collection_configurable_spec.rb'
+ - 'spec/mongoid/criteria/queryable/storable_spec.rb'
- 'spec/mongoid/document_spec.rb'
- 'spec/mongoid/errors/mongoid_error_spec.rb'
- 'spec/mongoid/persistence_context_spec.rb'
@@ -563,7 +565,7 @@ RSpec/RepeatedExample:
- 'spec/mongoid/factory_spec.rb'
- 'spec/mongoid/persistable/savable_spec.rb'
-# Offense count: 72
+# Offense count: 68
RSpec/RepeatedExampleGroupBody:
Exclude:
- 'spec/mongoid/association/embedded/embedded_in_spec.rb'
@@ -574,7 +576,6 @@ RSpec/RepeatedExampleGroupBody:
- 'spec/mongoid/association/referenced/belongs_to_spec.rb'
- 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb'
- - 'spec/mongoid/association/referenced/has_many/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_many_spec.rb'
- 'spec/mongoid/association/referenced/has_one_spec.rb'
- 'spec/mongoid/attributes/readonly_spec.rb'
@@ -625,20 +626,19 @@ RSpec/ScatteredLet:
- 'spec/mongoid/reloadable_spec.rb'
- 'spec/mongoid/scopable_spec.rb'
-# Offense count: 69
+# Offense count: 67
RSpec/ScatteredSetup:
Exclude:
- 'spec/integration/stringified_symbol_field_spec.rb'
- 'spec/mongoid/association/auto_save_spec.rb'
- 'spec/mongoid/association/referenced/belongs_to/proxy_spec.rb'
- 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb'
- - 'spec/mongoid/association/referenced/has_many/proxy_spec.rb'
- 'spec/mongoid/attributes/nested_spec.rb'
- 'spec/mongoid/changeable_spec.rb'
- 'spec/mongoid/copyable_spec.rb'
- 'spec/mongoid/persistable/deletable_spec.rb'
-# Offense count: 12
+# Offense count: 14
RSpec/StubbedMock:
Exclude:
- 'spec/mongoid/clients_spec.rb'
@@ -676,7 +676,7 @@ RSpec/VerifiedDoubles:
- 'spec/mongoid/validatable/associated_spec.rb'
- 'spec/rails/mongoid_spec.rb'
-# Offense count: 22
+# Offense count: 21
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforceForPrefixed.
Rails/Delegate:
@@ -687,7 +687,6 @@ Rails/Delegate:
- 'lib/mongoid/clients.rb'
- 'lib/mongoid/clients/options.rb'
- 'lib/mongoid/contextual/memory.rb'
- - 'lib/mongoid/contextual/mongo.rb'
- 'lib/mongoid/contextual/none.rb'
- 'lib/mongoid/criteria.rb'
- 'lib/mongoid/document.rb'
@@ -716,7 +715,7 @@ Rails/I18nLocaleAssignment:
- 'spec/mongoid/validatable/uniqueness_spec.rb'
- 'spec/support/macros.rb'
-# Offense count: 126
+# Offense count: 129
# Configuration parameters: ForbiddenMethods, AllowedMethods.
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
Rails/SkipsModelValidations:
@@ -798,11 +797,10 @@ Rails/TimeZoneAssignment:
- 'spec/support/macros.rb'
- 'spec/support/shared/time.rb'
-# Offense count: 6
+# Offense count: 5
Style/MultilineBlockChain:
Exclude:
- 'lib/mongoid/association/eager.rb'
- - 'lib/mongoid/association/referenced/has_many/proxy.rb'
- 'lib/mongoid/contextual/memory.rb'
- 'spec/mongoid/changeable_spec.rb'
- 'spec/mongoid/document_spec.rb'
@@ -833,7 +831,7 @@ Style/OptionalBooleanParameter:
- 'spec/support/models/address.rb'
- 'spec/support/models/name.rb'
-# Offense count: 242
+# Offense count: 240
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
diff --git a/docs/reference/associations.txt b/docs/reference/associations.txt
index 4cdbd63a2..ab3deb411 100644
--- a/docs/reference/associations.txt
+++ b/docs/reference/associations.txt
@@ -1267,7 +1267,7 @@ The child may become orphaned if it is ordinarily only referenced via the parent
label.bands.push(Band.first)
label.delete # Raises an error since bands is not empty.
- Band.first.delete # Will delete all associated albums.
+ Band.first.destroy # Will delete all associated albums.
Autosaving
diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt
index 220f0b742..d02bffc9d 100644
--- a/docs/reference/compatibility.txt
+++ b/docs/reference/compatibility.txt
@@ -32,7 +32,7 @@ specified Mongoid versions.
:class: compatibility-large no-padding
* - Mongoid
- - Driver 2.18
+ - Driver 2.19-2.18
- Driver 2.17-2.10
- Driver 2.9-2.7
@@ -83,7 +83,7 @@ is deprecated.
- |checkmark|
- |checkmark|
- D
- -
+ - D
-
-
-
@@ -97,13 +97,13 @@ is deprecated.
- |checkmark|
- |checkmark|
- |checkmark|
- - D
+ - |checkmark|
+ -
-
-
-
-
- |checkmark|
- - D
-
* - 8.0
@@ -250,6 +250,7 @@ and will be removed in a next version.
:class: compatibility-large no-padding
* - Mongoid
+ - MongoDB 7.0
- MongoDB 6.0
- MongoDB 5.0
- MongoDB 4.4
@@ -262,6 +263,7 @@ and will be removed in a next version.
- MongoDB 2.6
* - 9.0
+ - |checkmark|
- |checkmark|
- |checkmark|
- D
@@ -304,12 +306,15 @@ and will be removed in a next version.
- |checkmark|
- |checkmark|
- |checkmark|
+ - |checkmark|
- D
- D
- D
- D
* - 6.4 thru 7.3
+ -
+ -
-
- |checkmark|
- |checkmark|
@@ -353,7 +358,7 @@ are supported by Mongoid.
- |checkmark|
- |checkmark|
- |checkmark|
- - D [#rails-5-ruby-3.0]_
+ - |checkmark| [#rails-5-ruby-3.0]_
-
* - 8.0
diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt
index 7d1b699c3..af7fa7a95 100644
--- a/docs/reference/configuration.txt
+++ b/docs/reference/configuration.txt
@@ -813,14 +813,20 @@ the ``before_fork`` hook to close clients in the parent process
.. code-block:: ruby
on_worker_boot do
- Mongoid::Clients.clients.each do |name, client|
- client.close
- client.reconnect
+ if defined?(Mongoid)
+ Mongoid::Clients.clients.each do |name, client|
+ client.close
+ client.reconnect
+ end
+ else
+ raise "Mongoid is not loaded. You may have forgotten to enable app preloading."
end
end
before_fork do
- Mongoid.disconnect_clients
+ if defined?(Mongoid)
+ Mongoid.disconnect_clients
+ end
end
Unicorn
@@ -833,14 +839,20 @@ the ``before_fork`` hook to close clients in the parent process
.. code-block:: ruby
after_fork do |server, worker|
- Mongoid::Clients.clients.each do |name, client|
- client.close
- client.reconnect
+ if defined?(Mongoid)
+ Mongoid::Clients.clients.each do |name, client|
+ client.close
+ client.reconnect
+ end
+ else
+ raise "Mongoid is not loaded. You may have forgotten to enable app preloading."
end
end
before_fork do |server, worker|
- Mongoid.disconnect_clients
+ if defined?(Mongoid)
+ Mongoid.disconnect_clients
+ end
end
Passenger
@@ -856,9 +868,11 @@ before the workers are forked.
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
- Mongoid::Clients.clients.each do |name, client|
- client.close
- client.reconnect
+ if forked
+ Mongoid::Clients.clients.each do |name, client|
+ client.close
+ client.reconnect
+ end
end
end
end
diff --git a/docs/reference/queries.txt b/docs/reference/queries.txt
index 36e5004de..1fdd81e4f 100644
--- a/docs/reference/queries.txt
+++ b/docs/reference/queries.txt
@@ -2377,23 +2377,15 @@ document inserts, updates, and deletion.
Query Cache
===========
-The Ruby MongoDB driver provide query caching functionality. When enabled, the
+The Ruby MongoDB driver provides query caching functionality. When enabled, the
query cache saves the results of previously executed find and aggregation
queries and reuses them in the future instead of performing the queries again,
thus increasing application performance and reducing database load.
-The query cache has historically been provided by Mongoid but as of driver
-version 2.14.0, the query cache implementation has been moved into the driver.
-When Mongoid is used with driver version 2.14.0 or later, Mongoid delegates
-all work to the driver's query cache implementation.
-
Please review the `driver query cache documentation
`_
for details about the driver's query cache behavior.
-The rest of this section assumes that driver 2.14.0 or later is being used and
-Mongoid delegates to the driver's query cache.
-
Enabling Query Cache
--------------------
diff --git a/docs/release-notes/mongoid-8.1.txt b/docs/release-notes/mongoid-8.1.txt
index 5febf8c41..132dca49d 100644
--- a/docs/release-notes/mongoid-8.1.txt
+++ b/docs/release-notes/mongoid-8.1.txt
@@ -260,7 +260,7 @@ Added ``readonly!`` method and ``legacy_readonly`` feature flag
---------------------------------------------------------------
Mongoid 8.1 changes the meaning of read-only documents. In Mongoid 8.1 with
-this feature flag turned off, a document becomes read-only when calling the
+this feature flag set to ``false``, a document becomes read-only when calling the
``readonly!`` method:
.. code:: ruby
@@ -276,7 +276,7 @@ With this feature flag turned off, a ``ReadonlyDocument`` error will be
raised when destroying or deleting, as well as when saving or updating.
Prior to Mongoid 8.1 and in 8.1 with the ``legacy_readonly`` feature flag
-turned on, documents become read-only when they are projected (i.e. using
+set to ``true``, documents become read-only when they are projected (i.e. using
``#only`` or ``#without``).
.. code:: ruby
diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt
index 5e52492af..7dd57d659 100644
--- a/docs/release-notes/mongoid-9.0.txt
+++ b/docs/release-notes/mongoid-9.0.txt
@@ -74,9 +74,10 @@ Deprecated functionality removed
The following previously deprecated functionality is now removed:
-- ``Mongoid::QueryCache`` has been removed. Please replace it 1-for-1 with ``Mongo::QueryCache``.
+- The ``Mongoid::QueryCache`` module has been removed. Please replace any usages 1-for-1 with ``Mongo::QueryCache``.
The method ``Mongoid::QueryCache#clear_cache`` should be replaced with ``Mongo::QueryCache#clear``.
- All other methods and submodules are identically named.
+ All other methods and submodules are identically named. Refer to the `driver query cache documentation
+ `_ for more details.
- ``Document#as_json :compact`` option is removed. Please call ```#compact``
on the returned ``Hash`` object instead.
- ``Criteria#geo_near`` is removed as MongoDB server versions 4.2
@@ -365,6 +366,43 @@ of the ``index`` macro: ``partial_filter_expression``, ``weights``,
and Mongoid's functionality may not support such changes.
+BSON 5 and BSON::Decimal128 Fields
+----------------------------------
+
+When BSON 4 or earlier is present, any field declared as BSON::Decimal128 will
+return a BSON::Decimal128 value. When BSON 5 is present, however, any field
+declared as BSON::Decimal128 will return a BigDecimal value by default.
+
+.. code-block:: ruby
+
+ class Model
+ include Mongoid::Document
+
+ field :decimal_field, type: BSON::Decimal128
+ end
+
+ # under BSON <= 4
+ Model.first.decimal_field.class #=> BSON::Decimal128
+
+ # under BSON >= 5
+ Model.first.decimal_field.class #=> BigDecimal
+
+If you need literal BSON::Decimal128 values with BSON 5, you may instruct
+Mongoid to allow literal BSON::Decimal128 fields:
+
+.. code-block:: ruby
+
+ Model.first.decimal_field.class #=> BigDecimal
+
+ Mongoid.allow_bson5_decimal128 = true
+ Model.first.decimal_field.class #=> BSON::Decimal128
+
+.. note::
+
+ The ``allow_bson5_decimal128`` option only has any effect under
+ BSON 5 and later. BSON 4 and earlier ignore the setting entirely.
+
+
Bug Fixes and Improvements
--------------------------
@@ -394,3 +432,6 @@ This section will be for smaller bug fixes and improvements:
a non-numeric, non-string value that implements ``:to_d`` will return a string
rather than a ``BigDecimal``
`MONGOID-5507 `_.
+- Added support for serializing and deserializing BSON::ObjectId values
+ when passed as ActiveJob arguments
+ `MONGOID-5611 `_.
diff --git a/docs/tutorials.txt b/docs/tutorials.txt
index 33ab03954..d9f0a8510 100644
--- a/docs/tutorials.txt
+++ b/docs/tutorials.txt
@@ -14,3 +14,4 @@ Tutorials
tutorials/getting-started-rails6
tutorials/documents
tutorials/common-errors
+ tutorials/automatic-encryption
diff --git a/docs/tutorials/automatic-encryption.txt b/docs/tutorials/automatic-encryption.txt
new file mode 100644
index 000000000..dd7ce2890
--- /dev/null
+++ b/docs/tutorials/automatic-encryption.txt
@@ -0,0 +1,435 @@
+********************************************
+Automatic Client-Side Field Level Encryption
+********************************************
+
+.. default-domain:: mongodb
+
+.. contents:: On this page
+ :local:
+ :backlinks: none
+ :depth: 2
+ :class: singlecol
+
+
+Since version 4.2 MongoDB supports `Client-Side Field Level Encryption
+(CSFLE) `_. This is a feature
+that enables you to encrypt data in your application before you send it over the
+network to MongoDB. With CSFLE enabled, no MongoDB product has access to your
+data in an unencrypted form.
+
+You can set up CSFLE using the following mechanisms:
+
+* `Automatic Encryption `_:
+ Enables you to perform encrypted read and write operations without you having
+ to write code to specify how to encrypt fields.
+* `Explicit Encryption `_:
+ Enables you to perform encrypted read and write operations through your
+ MongoDB driver's encryption library. You must specify the logic for encryption
+ with this library throughout your application.
+
+Starting with version 9.0, Mongoid supports CSFLE's Automatic Encryption
+feature. This tutorial walks you through the process of setting up and using
+CSFLE in Mongoid.
+
+.. note::
+ This tutorial does not cover all CSLFE features.
+ You can find more information about MongoDB CSFLE in
+ `the server documentation. `_
+
+.. note::
+ If you want to use explicit FLE, please follow `the Ruby driver documentation.
+ `_
+
+
+Installation
+============
+
+You can find the detailed description of how to install the necessary
+dependencies in `the driver documentation. `_
+
+Note the version of the Ruby driver being used in your application and select
+the appropriate steps below.
+
+Install ``libmongocrypt``
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This can be done one of two ways.
+
+* Add the `libmongocrypt-helper gem `_ to
+ your ``Gemfile`` or
+* Download the ``libmongocrypt`` `release archive `_,
+ extract the version that matches your operating system, and set the
+ ``LIBMONGOCRYPT_PATH`` environment variable accordingly.
+
+Install the automatic encryption shared library (Ruby driver 2.19+)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you use the Ruby driver version 2.19 and above the automatic encryption
+shared library should be installed by following the instructions in the
+`MongoDB manual `_.
+
+The steps required are as follows:
+
+1. Navigate to the `MongoDB Download Center `_
+2. From the Version dropdown, select ``x.y.z (current)`` (the latest current version).
+3. In the Platform dropdown, select your platform.
+4. In the Package dropdown, select ``crypt_shared``.
+5. Click Download.
+
+Once extracted, ensure the full path to the library is configured within your
+``mongoid.yml`` as follows:
+
+.. code-block:: yaml
+
+ development:
+ clients:
+ default:
+ options:
+ auto_encryption_options:
+ extra_options:
+ crypt_shared_lib_path: '/path/to/mongo_crypt_v1.so'
+
+Install the ``mongocryptd`` (Ruby driver 2.18 or older)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are using an older version of the Ruby driver ``mongocryptd`` will
+need to be installed manually. ``mongocryptd`` comes pre-packaged with
+enterprise builds of the MongoDB server (versions 4.2 and newer).
+For installation instructions, see the `MongoDB manual `_.
+
+Add ``ffi`` to your Gemfile
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The MongoDB Ruby driver uses the `ffi gem `_ to call
+functions from ``libmongocrypt``. As this gem is not a dependency of
+the driver, it will need to be manually added to your ``Gemfile``:
+
+.. code-block:: ruby
+
+ gem 'ffi'
+
+Create a Customer Master Key
+============================
+
+A Customer Master Key (CMK) is used to encrypt Data Encryption Keys.
+The easiest way is to use a locally stored 96-bytes key. You can generate such
+a key using the following Ruby code:
+
+.. code-block:: ruby
+
+ require 'securerandom'
+
+ SecureRandom.hex(48) # => "f54ab...."
+
+Later in this tutorial we assume that the Customer Master Key is
+available from the ``CUSTOMER_MASTER_KEY`` environment variable.
+
+.. warning::
+
+ Using a local master key is insecure. It is recommended that you use a remote
+ Key Management Service to create and store your master key. To do so, follow
+ steps of the `"Set up a Remote Master Key" `_
+ in the MongoDB Client-Side Encryption documentation.
+
+ For more information about creating a master key, see the
+ `Create a Customer Master Key `_
+ section of the MongoDB manual.
+
+Configure Clients
+=================
+
+Automatic CSFLE requires some additional configuration for the MongoDB client.
+Assuming that your application has just one ``default`` client, you need to
+add the following to your ``mongoid.yml``:
+
+.. code-block:: yaml
+
+ development:
+ clients:
+ default:
+ uri: mongodb+srv://user:pass@yourcluster.mongodb.net/blog_development?retryWrites=true&w=majority
+ options:
+ auto_encryption_options: # This key enables automatic encryption
+ key_vault_namespace: 'encryption.__keyVault' # Database and collection to store data keys
+ kms_providers: # Tells the driver where to obtain master keys
+ local: # We use the local key in this tutorial
+ key: "<%= ENV['CUSTOMER_MASTER_KEY'] %>" # Key that we generated earlier
+ extra_options:
+ crypt_shared_lib_path: '/path/to/mongo_crypt_v1.so' # Only if you use the library
+
+
+Create a Data Encryption Key
+============================
+
+A Data Encryption Key (DEK) is the key you use to encrypt the fields in your
+MongoDB documents. You store your Data Encryption Key in your Key Vault
+collection encrypted with your CMK.
+
+To create a DEK in Mongoid you can use the
+``db:mongoid:encryption:create_data_key`` ``Rake`` task:
+
+.. code-block:: sh
+
+ % rake db:mongoid:encryption:create_data_key
+ Created data key with id: 'KImyywsTQWi1+cFYIHdtlA==' for kms provider: 'local' in key vault: 'encryption.__keyVault'.
+
+You can create multiple DEKs, if necessary.
+
+.. code-block:: sh
+
+ % rake db:mongoid:encryption:create_data_key
+ Created data key with id: 'Vxr5m+5cQISjDOruzZgE0w==' for kms provider: 'local' in key vault: 'encryption.__keyVault'.
+
+You can also provide an alternate name for the DEK. This allows you to reference
+the DEK by name when configuring encryption for your fields. It also allows you
+to dynamically assign a DEK to a field at runtime.
+
+.. code-block:: sh
+
+ % rake db:mongoid:encryption:create_data_key -- --key-alt-name=my_data_key
+ Created data key with id: 'yjF8hKmKQsqGeFGXlB9Sow==' with key alt name: 'my_data_key' for kms provider: 'local' in key vault: 'encryption.__keyVault'.
+
+
+Configure Encryption Schema
+===========================
+
+Now we can tell Mongoid what should be encrypted:
+
+.. code-block:: ruby
+
+ class Patient
+ include Mongoid::Document
+ include Mongoid::Timestamps
+
+ # Tells Mongoid what DEK should be used to encrypt fields of the document
+ # and its embedded documents.
+ encrypt_with key_id: 'KImyywsTQWi1+cFYIHdtlA=='
+
+ # This field is not encrypted.
+ field :category, type: String
+
+ # This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Random
+ # algorithm.
+ field :passport_id, type: String, encrypt: {
+ deterministic: false
+ }
+ # This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic
+ # algorithm.
+ field :blood_type, type: String, encrypt: {
+ deterministic: true
+ }
+ # This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Random
+ # algorithm and using a different data key.
+ field :ssn, type: Integer, encrypt: {
+ deterministic: false, key_id: 'Vxr5m+5cQISjDOruzZgE0w=='
+ }
+
+ embeds_one :insurance
+ end
+
+ class Insurance
+ include Mongoid::Document
+ include Mongoid::Timestamps
+
+ field :insurer, type: String
+
+ # This field is encrypted using AEAD_AES_256_CBC_HMAC_SHA_512-Random
+ # algorithm using the key which alternate name is stored in the
+ # policy_number_key field.
+ field :policy_number, type: Integer, encrypt: {
+ deterministic: false,
+ key_name_field: :policy_number_key
+ }
+
+ embedded_in :patient
+ end
+
+.. note::
+ If you are developing a Rails application, it is recommended to set
+ ``preload_models`` to ``true`` in ``mongoid.yml``. This will ensure that
+ Mongoid loads all models before the application starts, and the encryption
+ schema is configured before any data is read or written.
+
+Known Limitations
+~~~~~~~~~~~~~~~~~
+
+* MongoDB CSFLE has some limitations that are described in
+ `the server documentation. `_
+ These limitations also apply to Mongoid.
+* Mongoid does not support encryption of ``embeds_many`` relations.
+* If you use ``:key_name_field`` option, the field must be encrypted using
+ non-deterministic algorithm. To encrypt your field deterministically, you must
+ specify ``:key_id`` option instead.
+
+Working with Data
+=================
+
+Automatic CSFLE usage is transparent in many situations.
+
+.. note::
+ In code examples below we assume that there is a variable ``unencrypted_client``
+ that is a ``MongoClient`` connected to the same database but without encryption.
+ We use this client to demonstrate what is actually persisted in the database.
+
+Documents can be created as usual, fields will be encrypted and decrypted
+according to the configuration:
+
+.. code-block:: ruby
+
+ Patient.create!(
+ category: 'ER',
+ passport_id: '123456',
+ blood_type: 'AB+',
+ ssn: 98765,
+ insurance: Insurance.new(insurer: 'TK', policy_number: 123456, policy_number_key: 'my_data_key')
+ )
+
+ # Fields are encrypted in the database
+ unencrypted_client['patients'].find.first
+ # =>
+ # {"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4292'),
+ # "category"=>"ER",
+ # "passport_id"=>,
+ # "blood_type"=>,
+ # "ssn"=>,
+ # "insurance"=>{"_id"=>BSON::ObjectId('6446a1d046ebfd701f9f4293'), "insurer"=>"TK", "policy_number"=>}, "policy_number_key"=>"my_data_key"}
+
+Fields encrypted using a deterministic algorithm can be queried. Only exact match
+queries are supported. For more details please consult `the server documentation
+`_.
+
+.. code-block:: ruby
+
+ # We can find documents by deterministically encrypted fields.
+ Patient.where(blood_type: "AB+").to_a
+ # => [#]
+
+Encryption Key Management
+=========================
+
+Customer Master Keys
+~~~~~~~~~~~~~~~~~~~~
+
+Your Customer Master Key is the key you use to encrypt your Data Encryption Keys.
+MongoDB automatically encrypts Data Encryption Keys using the specified CMK
+during Data Encryption Key creation.
+
+The CMK is the most sensitive key in CSFLE. If your CMK is compromised, all of
+your encrypted data can be decrypted.
+
+.. important::
+ Ensure you store your Customer Master Key (CMK) on a remote KMS.
+
+ To learn more about why you should use a remote KMS, see `Reasons to Use a Remote KMS. `_
+
+ To view a list of all supported KMS providers, see the `KMS Providers `_ page.
+
+MongoDB CSFLE supports the following Key Management System (KMS) providers:
+ * `Amazon Web Services KMS `_
+ * `Azure Key Vault `_
+ * `Google Cloud Platform KMS `_
+ * Any KMIP Compliant Key Management System
+ * Local Key Provider (for testing only)
+
+Data Encryption Keys
+~~~~~~~~~~~~~~~~~~~~
+
+Data Encryption Keys can be created using the
+``db:mongoid:encryption:create_data_key`` ``Rake`` task. By default they are
+stored on the same cluster as the database.
+However, it might be a good idea to store the keys separately. This can be
+done by specifying a key vault client in ``mongoid.yml``:
+
+.. code-block:: yaml
+
+ development:
+ clients:
+ key_vault:
+ uri: mongodb+srv://user:pass@anothercluster.mongodb.net/blog_development?retryWrites=true&w=majority
+ default:
+ uri: mongodb+srv://user:pass@yourcluster.mongodb.net/blog_development?retryWrites=true&w=majority
+ options:
+ auto_encryption_options:
+ key_vault_client: :key_vault # Client to connect to key vault
+ # ...
+
+Encryption Keys Rotation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can rotate encryption keys using the ``rewrap_many_data_key`` method
+of the Ruby driver. This method automatically decrypts multiple data encryption
+keys and re-encrypts them using a specified CMK. It then updates
+the rotated keys in the key vault collection. This method allows you to rotate
+encryption keys based on two optional arguments:
+
+* A filter used to specify which keys to rotate. If no data key matches the
+ given filter, no keys will be rotated. Omit the filter to rotate all keys in
+ your key vault collection.
+* An object that represents a new CMK. Omit this object to rotate the data
+ keys using their current CMKs.
+
+Here is an example of rotating keys using AWS KMS:
+
+.. code-block:: ruby
+
+ # Create a key vault client
+ key_vault_client = Mongo::Client.new('mongodb+srv://user:pass@yourcluster.mongodb.net')
+ # Or, if you declared the key value client in mongoid.yml, use it
+ key_vault_client = Mongoid.client(:key_vault)
+
+ # Create the encryption object
+ encryption = Mongo::ClientEncryption.new(
+ key_vault_client,
+ key_vault_namespace: 'encryption.__keyVault',
+ kms_providers: {
+ aws: {
+ "accessKeyId": "",
+ "secretAccessKey": ""
+ }
+ }
+ )
+
+ encryption.rewrap_many_data_key(
+ {}, # We want to rewrap all keys
+ {
+ provider: 'aws',
+ master_key: {
+ region: 'us-east-2',
+ key: 'arn:aws:kms:us-east-2:...'
+ }
+ }
+ )
+
+Adding Automatic Encryption To Existing Project
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+MongoDB automatic CSFLE supports encryption in place. You can enable encryption
+for your existing database, and will still able to read unencrypted data.
+All data written to the database will be encrypted. However, as soon as the
+encryption is enabled, all query operations will use encrypted data:
+
+.. code-block:: ruby
+
+ # We assume that there are two documents in the database, one created without
+ # encryption enabled, and one with encryption.
+
+ # We can still read both.
+ Patient.all.to_a
+ # =>
+ # [#,
+ # #]
+
+ # But when we query, we can see only the latter one.
+ Patient.where(blood_type: 'AB+').to_a
+ # => [#]
+
+If you want to encrypt the existing database, it can be achieved by reading
+and writing back all data, even without any changes. If you decide to do so,
+please keep the following in mind:
+
+* Validate the integrity of existing data for consistent fidelity. CSFLE is
+ type sensitive - for example you cannot store integers in a field that is
+ declared as ``String``.
+* For strings, make sure that empty values are always empty strings or just
+ not set, but not ``nil`` (CSFLE doesn't support native ``null``).
+* This operation requires application downtime.
diff --git a/gemfiles/standard.rb b/gemfiles/standard.rb
index 2f968db0a..e5c4dd392 100644
--- a/gemfiles/standard.rb
+++ b/gemfiles/standard.rb
@@ -16,7 +16,8 @@ def standard_dependencies
end
group :test do
- gem 'rspec', '~> 3.10'
+ gem 'rspec', '~> 3.12'
+ gem 'activejob'
gem 'timecop'
gem 'rspec-retry'
gem 'benchmark-ips'
@@ -36,6 +37,6 @@ def standard_dependencies
end
if ENV['FLE'] == 'helper'
- gem 'libmongocrypt-helper', '~> 1.7.0'
+ gem 'libmongocrypt-helper', '~> 1.8.0'
end
end
diff --git a/lib/mongoid/association/embedded/embedded_in/proxy.rb b/lib/mongoid/association/embedded/embedded_in/proxy.rb
index 28653b35c..d91d446d3 100644
--- a/lib/mongoid/association/embedded/embedded_in/proxy.rb
+++ b/lib/mongoid/association/embedded/embedded_in/proxy.rb
@@ -24,7 +24,7 @@ class Proxy < Association::One
#
# @return [ In ] The proxy.
def initialize(base, target, association)
- init(base, target, association) do
+ super do
characterize_one(_target)
bind_one
end
@@ -71,9 +71,7 @@ def binding
#
# @param [ Mongoid::Document ] document The document to set the association metadata on.
def characterize_one(document)
- return if _base._association
-
- _base._association = _association.inverse_association(document)
+ _base._association ||= _association.inverse_association(document)
end
# Are we able to persist this association?
diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb
index 59a692613..a3afc96fb 100644
--- a/lib/mongoid/association/embedded/embeds_many/proxy.rb
+++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb
@@ -16,6 +16,68 @@ class EmbedsMany
class Proxy < Association::Many
include Batchable
+ # Class-level methods for the Proxy class.
+ module ClassMethods
+
+ # Returns the eager loader for this association.
+ #
+ # @param [ Array ] associations The
+ # associations to be eager loaded
+ # @param [ Array ] docs The parent documents
+ # that possess the given associations, which ought to be
+ # populated by the eager-loaded documents.
+ #
+ # @return [ Mongoid::Association::Embedded::Eager ]
+ def eager_loader(associations, docs)
+ Eager.new(associations, docs)
+ end
+
+ # Returns true if the association is an embedded one. In this case
+ # always true.
+ #
+ # @example Is the association embedded?
+ # Association::Embedded::EmbedsMany.embedded?
+ #
+ # @return [ true ] true.
+ def embedded?
+ true
+ end
+
+ # Returns the suffix of the foreign key field, either "_id" or "_ids".
+ #
+ # @example Get the suffix for the foreign key.
+ # Association::Embedded::EmbedsMany.foreign_key_suffix
+ #
+ # @return [ nil ] nil.
+ def foreign_key_suffix
+ nil
+ end
+ end
+
+ extend ClassMethods
+
+ # Instantiate a new embeds_many association.
+ #
+ # @example Create the new association.
+ # Many.new(person, addresses, association)
+ #
+ # @param [ Mongoid::Document ] base The document this association hangs off of.
+ # @param [ Array ] target The child documents of the association.
+ # @param [ Mongoid::Association::Relatable ] association The association metadata.
+ #
+ # @return [ Many ] The proxy.
+ def initialize(base, target, association)
+ super do
+ _target.each_with_index do |doc, index|
+ integrate(doc)
+ doc._index = index
+ end
+ update_attributes_hash
+ @_unscoped = _target.dup
+ @_target = scope(_target)
+ end
+ end
+
# Appends a document or array of documents to the association. Will set
# the parent and update the index in the process.
#
@@ -28,12 +90,14 @@ class Proxy < Association::Many
# @param [ Mongoid::Document... ] *args Any number of documents.
def <<(*args)
docs = args.flatten
+ return unless docs.any?
return concat(docs) if docs.size > 1
if (doc = docs.first)
append(doc)
doc.save if persistable? && !_assigning?
end
+
self
end
@@ -161,6 +225,23 @@ def delete(document)
end
end
+ # Mongoid::Extensions::Array defines Array#delete_one, so we need
+ # to make sure that method behaves reasonably on proxies, too.
+ alias_method :delete_one, :delete
+
+ # Removes a single document from the collection *in memory only*.
+ # It will *not* persist the change.
+ #
+ # @param [ Document ] document The document to delete.
+ #
+ # @api private
+ def _remove(document)
+ _target.delete_one(document)
+ _unscoped.delete_one(document)
+ update_attributes_hash
+ reindex
+ end
+
# Delete all the documents in the association without running callbacks.
#
# @example Delete all documents from the association.
@@ -183,18 +264,14 @@ def delete_all(conditions = {})
# doc.state == "GA"
# end
#
- # @return [ Many | Enumerator ] The association or an enumerator if no
- # block was provided.
+ # @return [ EmbedsMany::Proxy | Enumerator ] The proxy or an
+ # enumerator if no block was provided.
def delete_if
- if block_given?
- dup_target = _target.dup
- dup_target.each do |doc|
- delete(doc) if yield(doc)
- end
- self
- else
- super
- end
+ return super unless block_given?
+
+ _target.dup.each { |doc| delete(doc) if yield doc }
+
+ self
end
# Destroy all the documents in the association whilst running callbacks.
@@ -256,37 +333,13 @@ def find(...)
criteria.find(...)
end
- # Instantiate a new embeds_many association.
- #
- # @example Create the new association.
- # Many.new(person, addresses, association)
- #
- # @param [ Mongoid::Document ] base The document this association hangs off of.
- # @param [ Array ] target The child documents of the association.
- # @param [ Mongoid::Association::Relatable ] association The association metadata.
- #
- # @return [ Many ] The proxy.
- def initialize(base, target, association)
- init(base, target, association) do
- _target.each_with_index do |doc, index|
- integrate(doc)
- doc._index = index
- end
- update_attributes_hash
- @_unscoped = _target.dup
- @_target = scope(_target)
- end
- end
-
# Get all the documents in the association that are loaded into memory.
#
# @example Get the in memory documents.
# relation.in_memory
#
# @return [ Array ] The documents in memory.
- def in_memory
- _target
- end
+ alias_method :in_memory, :_target
# Pop documents off the association. This can be a single document or
# multiples, and will automatically persist the changes.
@@ -300,17 +353,12 @@ def in_memory
# @param [ Integer ] count The number of documents to pop, or 1 if not
# provided.
#
- # @return [ Mongoid::Document | Array ] The popped document(s).
+ # @return [ Mongoid::Document | Array | nil ] The popped document(s).
def pop(count = nil)
- if count
- if (docs = _target[_target.size - count, _target.size])
- docs.each { |doc| delete(doc) }
- end
- else
- delete(_target[-1])
- end.tap do
- update_attributes_hash
- end
+ return [] if count&.zero?
+
+ docs = _target.last(count || 1).each { |doc| delete(doc) }
+ count.nil? || docs.empty? ? docs.first : docs
end
# Shift documents off the association. This can be a single document or
@@ -325,17 +373,12 @@ def pop(count = nil)
# @param [ Integer ] count The number of documents to shift, or 1 if not
# provided.
#
- # @return [ Mongoid::Document | Array ] The shifted document(s).
+ # @return [ Mongoid::Document | Array | nil ] The shifted document(s).
def shift(count = nil)
- if count
- if !_target.empty? && (docs = _target[0, count])
- docs.each { |doc| delete(doc) }
- end
- else
- delete(_target[0])
- end.tap do
- update_attributes_hash
- end
+ return [] if count&.zero?
+
+ docs = _target.first(count || 1).each { |doc| delete(doc) }
+ count.nil? || docs.empty? ? docs.first : docs
end
# Substitutes the supplied target documents for the existing documents
@@ -369,8 +412,15 @@ def unscoped
private
+ # The internal unscoped documents.
+ #
+ # @param [ Array ] docs The documents.
+ #
+ # @return [ Array ] The unscoped docs.
+ attr_accessor :_unscoped
+
def object_already_related?(document)
- _target.any? { |existing| existing._id && existing === document }
+ _target.any? { |existing| existing._id && existing == document }
end
# Appends the document to the target array, updating the index on the
@@ -408,21 +458,6 @@ def criteria
_association.criteria(_base, _target)
end
- # Deletes one document from the target and unscoped.
- #
- # @api private
- #
- # @example Delete one document.
- # relation.delete_one(doc)
- #
- # @param [ Mongoid::Document ] document The document to delete.
- def delete_one(document)
- _target.delete_one(document)
- _unscoped.delete_one(document)
- update_attributes_hash
- reindex
- end
-
# Integrate the document into the association. will set its metadata and
# attempt to bind the inverse.
#
@@ -521,26 +556,6 @@ def remove_all(conditions = {}, method = :delete)
removed
end
- # Get the internal unscoped documents.
- #
- # @example Get the unscoped documents.
- # relation._unscoped
- #
- # @return [ Array ] The unscoped documents.
- def _unscoped
- @_unscoped ||= []
- end
-
- # Set the internal unscoped documents.
- #
- # @example Set the unscoped documents.
- # relation._unscoped = docs
- #
- # @param [ Array ] docs The documents.
- #
- # @return [ Array ] The unscoped docs.
- attr_writer :_unscoped
-
# Returns a list of attributes hashes for each document.
#
# @return [ Array ] The list of attributes hashes
@@ -558,42 +573,6 @@ def update_attributes_hash
_base.attributes.merge!(_association.store_as => _target.map(&:attributes))
end
end
-
- class << self
- # Returns the eager loader for this association.
- #
- # @param [ Array ] associations The
- # associations to be eager loaded
- # @param [ Array ] docs The parent documents
- # that possess the given associations, which ought to be
- # populated by the eager-loaded documents.
- #
- # @return [ Mongoid::Association::Embedded::Eager ]
- def eager_loader(associations, docs)
- Eager.new(associations, docs)
- end
-
- # Returns true if the association is an embedded one. In this case
- # always true.
- #
- # @example Is the association embedded?
- # Association::Embedded::EmbedsMany.embedded?
- #
- # @return [ true ] true.
- def embedded?
- true
- end
-
- # Returns the suffix of the foreign key field, either "_id" or "_ids".
- #
- # @example Get the suffix for the foreign key.
- # Association::Embedded::EmbedsMany.foreign_key_suffix
- #
- # @return [ nil ] nil.
- def foreign_key_suffix
- nil
- end
- end
end
end
end
diff --git a/lib/mongoid/association/embedded/embeds_one/proxy.rb b/lib/mongoid/association/embedded/embeds_one/proxy.rb
index eccbb6ded..5dbe9ccb0 100644
--- a/lib/mongoid/association/embedded/embeds_one/proxy.rb
+++ b/lib/mongoid/association/embedded/embeds_one/proxy.rb
@@ -33,7 +33,7 @@ class Proxy < Association::One
# @param [ Mongoid::Document ] target The child document in the association.
# @param [ Mongoid::Association::Relatable ] association The association metadata.
def initialize(base, target, association)
- init(base, target, association) do
+ super do
characterize_one(_target)
bind_one
characterize_one(_target)
@@ -53,58 +53,20 @@ def initialize(base, target, association)
#
# @return [ Mongoid::Document | nil ] The association or nil.
def substitute(replacement)
- if replacement != self
- if _assigning?
- _base.add_atomic_unset(_target) unless replacement
- else
- # The associated object will be replaced by the below update if non-nil, so only
- # run the callbacks and state-changing code by passing persist: false in that case.
- _target.destroy(persist: !replacement) if persistable?
-
- # A little explanation on why this is needed... Say we have three assignments:
- #
- # canvas.palette = palette
- # canvas.palette = nil
- # canvas.palette = palette
- # Where canvas embeds_one palette.
- #
- # Previously, what was happening was, on the first assignment,
- # palette was considered a "new record" (new_record?=true) and
- # thus palette was being inserted into the database. However,
- # on the third assignment, we're trying to reassign the palette,
- # palette is no longer considered a new record, because it had
- # been inserted previously. This is not exactly accurate,
- # because the second assignment ultimately removed the palette
- # from the database, so it needs to be reinserted. Since the
- # palette's new_record is false, Mongoid ends up "updating" the
- # document, which doesn't reinsert it into the database.
- #
- # The change I introduce here, respecifies palette as a "new
- # record" when it gets removed from the database, so if it is
- # reassigned, it will be reinserted into the database.
- _target.new_record = true
- end
- unbind_one
- unless replacement
- update_attributes_hash(replacement)
-
- # when `touch: true` is the default (see MONGOID-5016), creating
- # an embedded document will touch the parent, and will cause the
- # _descendants list to be initialized and memoized. If the object
- # is then deleted, we need to make sure and un-memoize that list,
- # otherwise when the update happens, the memoized _descendants list
- # gets used and the "deleted" subdocument gets added again.
- _reset_memoized_descendants!
-
- return nil
- end
- replacement = Factory.build(klass, replacement) if replacement.is_a?(::Hash)
- self._target = replacement
- characterize_one(_target)
- bind_one
- update_attributes_hash(_target)
- _target.save if persistable?
+ return self if replacement == self
+
+ if _assigning?
+ _base.add_atomic_unset(_target) unless replacement
+ else
+ update_target_when_not_assigning(replacement)
end
+
+ unbind_one
+
+ return nil if replace_with_nil_document(replacement)
+
+ replace_with(replacement)
+
self
end
@@ -144,6 +106,79 @@ def update_attributes_hash(replacement)
end
end
+ # When not ``_assigning?``, the target may need to be destroyed, and
+ # may need to be marked as a new record. This method takes care of
+ # that, with extensive documentation for why it is necessary.
+ #
+ # @param [ Mongoid::Document ] replacement The replacement document.
+ def update_target_when_not_assigning(replacement)
+ # The associated object will be replaced by the below update if non-nil, so only
+ # run the callbacks and state-changing code by passing persist: false in that case.
+ _target.destroy(persist: !replacement) if persistable?
+
+ # A little explanation on why this is needed... Say we have three assignments:
+ #
+ # canvas.palette = palette
+ # canvas.palette = nil
+ # canvas.palette = palette
+ # Where canvas embeds_one palette.
+ #
+ # Previously, what was happening was, on the first assignment,
+ # palette was considered a "new record" (new_record?=true) and
+ # thus palette was being inserted into the database. However,
+ # on the third assignment, we're trying to reassign the palette,
+ # palette is no longer considered a new record, because it had
+ # been inserted previously. This is not exactly accurate,
+ # because the second assignment ultimately removed the palette
+ # from the database, so it needs to be reinserted. Since the
+ # palette's new_record is false, Mongoid ends up "updating" the
+ # document, which doesn't reinsert it into the database.
+ #
+ # The change I introduce here, respecifies palette as a "new
+ # record" when it gets removed from the database, so if it is
+ # reassigned, it will be reinserted into the database.
+ _target.new_record = true
+ end
+
+ # Checks the argument. If it is non-nil, this method does nothing.
+ # Otherwise, it performs the logic for replacing the current
+ # document with nil.
+ #
+ # @param [ Mongoid::Document | Hash | nil ] replacement The document
+ # to check.
+ #
+ # @return [ true | false ] Whether a nil document was substituted
+ # or not.
+ def replace_with_nil_document(replacement)
+ return false if replacement
+
+ update_attributes_hash(replacement)
+
+ # when `touch: true` is the default (see MONGOID-5016), creating
+ # an embedded document will touch the parent, and will cause the
+ # _descendants list to be initialized and memoized. If the object
+ # is then deleted, we need to make sure and un-memoize that list,
+ # otherwise when the update happens, the memoized _descendants list
+ # gets used and the "deleted" subdocument gets added again.
+ _reset_memoized_descendants!
+
+ true
+ end
+
+ # Replaces the current document with the given replacement
+ # document, which may be a Hash of attributes.
+ #
+ # @param [ Mongoid::Document | Hash ] replacement The document to
+ # substitute in place of the current document.
+ def replace_with(replacement)
+ replacement = Factory.build(klass, replacement) if replacement.is_a?(::Hash)
+ self._target = replacement
+ characterize_one(_target)
+ bind_one
+ update_attributes_hash(_target)
+ _target.save if persistable?
+ end
+
class << self
# Returns the eager loader for this association.
#
diff --git a/lib/mongoid/association/macros.rb b/lib/mongoid/association/macros.rb
index e35a9d2d2..627cb39be 100644
--- a/lib/mongoid/association/macros.rb
+++ b/lib/mongoid/association/macros.rb
@@ -35,10 +35,15 @@ module Macros
# @api private
class_attribute :aliased_associations
+ # @return [ Set ] The set of associations that are configured
+ # with :store_as parameter.
+ class_attribute :stored_as_associations
+
self.embedded = false
self.embedded_relations = BSON::Document.new
self.relations = BSON::Document.new
self.aliased_associations = {}
+ self.stored_as_associations = Set.new
end
# This is convenience for libraries still on the old API.
@@ -51,6 +56,7 @@ def associations
relations
end
+ # Class methods for associations.
module ClassMethods
# Adds the association back to the parent document. This macro is
@@ -219,6 +225,7 @@ def define_association!(macro_name, name, options = {}, &block)
self.relations = relations.merge(name => assoc)
if assoc.embedded? && assoc.respond_to?(:store_as) && assoc.store_as != name
aliased_associations[assoc.store_as] = name
+ stored_as_associations << assoc.store_as
end
end
end
diff --git a/lib/mongoid/association/proxy.rb b/lib/mongoid/association/proxy.rb
index 2650eb7c2..4c15658fc 100644
--- a/lib/mongoid/association/proxy.rb
+++ b/lib/mongoid/association/proxy.rb
@@ -10,13 +10,28 @@ module Association
class Proxy
extend Forwardable
- UNFORWARDABLE_METHODS = /\A(?:__.*|send|object_id|equal\?|respond_to\?|respond_to_missing\?|tap|public_send|extend_proxy|extend_proxies)\z/.freeze
+ # Specific methods to prevent from being undefined.
+ #
+ # @api private
+ KEEP_METHODS = %i[
+ send
+ object_id
+ equal?
+ respond_to?
+ respond_to_missing?
+ tap
+ public_send
+ extend_proxy
+ extend_proxies
+ ].freeze
alias_method :extend_proxy, :extend
# We undefine most methods to get them sent through to the target.
instance_methods.each do |method|
- undef_method(method) unless UNFORWARDABLE_METHODS.match?(method)
+ next if method.to_s.start_with?('__') || KEEP_METHODS.include?(method)
+
+ undef_method(method)
end
include Threaded::Lifecycle
@@ -42,16 +57,12 @@ class Proxy
def_delegators :binding, :bind_one, :unbind_one
def_delegator :_base, :collection_name
- # Convenience for setting the target and the association metadata properties since
- # all proxies will need to do this.
- #
- # @example Initialize the proxy.
- # proxy.init(person, name, association)
+ # Sets the target and the association metadata properties.
#
# @param [ Mongoid::Document ] base The base document on the proxy.
# @param [ Mongoid::Document | Array ] target The target of the proxy.
# @param [ Mongoid::Association::Relatable ] association The association metadata.
- def init(base, target, association)
+ def initialize(base, target, association)
@_base = base
@_target = target
@_association = association
diff --git a/lib/mongoid/association/referenced/belongs_to/proxy.rb b/lib/mongoid/association/referenced/belongs_to/proxy.rb
index aae93ee97..0735e6641 100644
--- a/lib/mongoid/association/referenced/belongs_to/proxy.rb
+++ b/lib/mongoid/association/referenced/belongs_to/proxy.rb
@@ -25,7 +25,7 @@ class Proxy < Association::One
# association.
# @param [ Mongoid::Association::Relatable ] association The association object.
def initialize(base, target, association)
- init(base, target, association) do
+ super do
characterize_one(_target)
bind_one
end
diff --git a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb
index fb384dd53..04c454688 100644
--- a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb
+++ b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb
@@ -14,6 +14,33 @@ class HasAndBelongsToMany
# which must be loaded.
class Proxy < Referenced::HasMany::Proxy
+ # Class-level methods for HasAndBelongsToMany::Proxy
+ module ClassMethods
+
+ # Get the eager loader object for this type of association.
+ #
+ # @example Get the eager loader object
+ #
+ # @param [ Mongoid::Association::Relatable ] association The association metadata.
+ # @param [ Array ] docs The array of documents.
+ def eager_loader(association, docs)
+ Eager.new(association, docs)
+ end
+
+ # Returns true if the association is an embedded one. In this case
+ # always false.
+ #
+ # @example Is this association embedded?
+ # Referenced::ManyToMany.embedded?
+ #
+ # @return [ false ] Always false.
+ def embedded?
+ false
+ end
+ end
+
+ extend ClassMethods
+
# Appends a document or array of documents to the association. Will set
# the parent and update the index in the process.
#
@@ -80,21 +107,7 @@ def concat(documents)
docs = []
inserts = []
- documents.each do |doc|
- next unless doc
-
- append(doc)
- if persistable? || _creating?
- ids[doc.public_send(_association.primary_key)] = true
- save_or_delay(doc, docs, inserts)
- else
- existing = _base.public_send(foreign_key)
- unless existing.include?(doc.public_send(_association.primary_key))
- existing.push(doc.public_send(_association.primary_key)) and unsynced(_base, foreign_key)
- end
- end
- end
-
+ documents.each { |doc| append_document(doc, ids, docs, inserts) }
_base.push(foreign_key => ids.keys) if persistable? || _creating?
persist_delayed(docs, inserts)
self
@@ -143,6 +156,10 @@ def delete(document)
doc
end
+ # Mongoid::Extensions::Array defines Array#delete_one, so we need
+ # to make sure that method behaves reasonably on proxies, too.
+ alias_method :delete_one, :delete
+
# Removes all associations between the base document and the target
# documents by deleting the foreign keys and the references, orphaning
# the target documents in the process.
@@ -152,43 +169,10 @@ def delete(document)
#
# @param [ Array ] replacement The replacement documents.
def nullify(replacement = [])
- _target.each do |doc|
- execute_callback :before_remove, doc
- end
-
- unless _association.forced_nil_inverse?
- ipk = if (field = _association.options[:inverse_primary_key])
- _base.public_send(field)
- else
- _base._id
- end
- if replacement
- objects_to_clear = _base.public_send(foreign_key) - replacement.collect do |object|
- object.public_send(_association.primary_key)
- end
- criteria(objects_to_clear).pull(inverse_foreign_key => ipk)
- else
- criteria.pull(inverse_foreign_key => ipk)
- end
- end
-
+ _target.each { |doc| execute_callback :before_remove, doc }
+ cleanup_inverse_for(replacement) unless _association.forced_nil_inverse?
_base.set(foreign_key => _base.public_send(foreign_key).clear) if persistable?
-
- after_remove_error = nil
-
- many_to_many = _target.clear do |doc|
- unbind_one(doc)
- doc.changed_attributes.delete(inverse_foreign_key) unless _association.forced_nil_inverse?
- begin
- execute_callback :after_remove, doc
- rescue StandardError => e
- after_remove_error = e
- end
- end
-
- raise after_remove_error if after_remove_error
-
- many_to_many
+ clear_target_for_nullify
end
alias_method :nullify_all, :nullify
@@ -200,7 +184,7 @@ def nullify(replacement = [])
# deletion.
#
# @example Replace the association.
- # person.preferences.substitute([ new_post ])
+ # person.preferences.substitute([ new_post ])
#
# @param [ Array ] replacement The replacement target.
#
@@ -245,14 +229,11 @@ def clear_foreign_key_changes
#
# @api private
def reset_foreign_key_changes
- if _base.changed_attributes.key?(foreign_key)
- fk = _base.changed_attributes[foreign_key].dup
- yield if block_given?
- _base.changed_attributes[foreign_key] = fk
- else
- yield if block_given?
- clear_foreign_key_changes
- end
+ prior_fk_change = _base.changed_attributes.key?(foreign_key)
+ fk = _base.changed_attributes[foreign_key].dup
+ yield if block_given?
+ _base.changed_attributes[foreign_key] = fk
+ clear_foreign_key_changes unless prior_fk_change
end
# Appends the document to the target array, updating the index on the
@@ -317,36 +298,86 @@ def criteria(id_list = nil)
# @param [ Mongoid::Document ] doc The document to flag.
# @param [ Symbol ] key The key to flag on the document.
#
- # @return [ true ] true.
+ # @return [ true ] The value true.
def unsynced(doc, key)
doc._synced[key] = false
true
end
- class << self
+ # Does the cleanup for the inverse of the association when
+ # replacing the relation with another list of documents.
+ #
+ # @param [ Array | nil ] replacement The list of documents
+ # that will replace the current list.
+ def cleanup_inverse_for(replacement)
+ if replacement
+ new_ids = replacement.collect { |doc| doc.public_send(_association.primary_key) }
+ objects_to_clear = _base.public_send(foreign_key) - new_ids
+ criteria(objects_to_clear).pull(inverse_foreign_key => inverse_primary_key)
+ else
+ criteria.pull(inverse_foreign_key => inverse_primary_key)
+ end
+ end
- # Get the Eager object for this type of association.
- #
- # @example Get the eager loader object
- #
- # @param [ Mongoid::Association::Relatable ] association The association metadata.
- # @param [ Array ] docs The array of documents.
- #
- # @return [ Mongoid::Association::Referenced::HasAndBelongsToMany::Eager ]
- # The eager loader.
- def eager_loader(association, docs)
- Eager.new(association, docs)
+ # The inverse primary key
+ #
+ # @return [ Object ] the inverse primary key
+ def inverse_primary_key
+ if (field = _association.options[:inverse_primary_key])
+ _base.public_send(field)
+ else
+ _base._id
end
+ end
- # Returns true if the association is an embedded one. In this case
- # always false.
- #
- # @example Is this association embedded?
- # Referenced::ManyToMany.embedded?
- #
- # @return [ false ] Always false.
- def embedded?
- false
+ # Clears the _target list and executes callbacks for each document.
+ # If an exception occurs in an after_remove hook, the exception is
+ # saved, the processing completes, and *then* the exception is
+ # re-raised.
+ #
+ # @return [ Array ] The replacement documents.
+ def clear_target_for_nullify
+ after_remove_error = nil
+ many_to_many = _target.clear do |doc|
+ unbind_one(doc)
+ doc.changed_attributes.delete(inverse_foreign_key) unless _association.forced_nil_inverse?
+
+ begin
+ execute_callback :after_remove, doc
+ rescue StandardError => e
+ after_remove_error = e
+ end
+ end
+
+ raise after_remove_error if after_remove_error
+
+ many_to_many
+ end
+
+ # Processes a single document as part of a ``concat`` command.
+ #
+ # @param [ Mongoid::Document ] doc The document to append.
+ # @param [ Hash ] ids The mapping of primary keys that have been
+ # visited.
+ # @param [ Array ] docs The list of new docs to be inserted later,
+ # in bulk.
+ # @param [ Array ] inserts The list of Hashes of attributes that will
+ # be inserted, corresponding to the ``docs`` list.
+ def append_document(doc, ids, docs, inserts)
+ return unless doc
+
+ append(doc)
+
+ pk = doc.public_send(_association.primary_key)
+ if persistable? || _creating?
+ ids[pk] = true
+ save_or_delay(doc, docs, inserts)
+ else
+ existing = _base.public_send(foreign_key)
+ return if existing.include?(pk)
+
+ existing.push(pk)
+ unsynced(_base, foreign_key)
end
end
end
diff --git a/lib/mongoid/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb
index 81fb68f2e..b9a75fe34 100644
--- a/lib/mongoid/association/referenced/has_many/proxy.rb
+++ b/lib/mongoid/association/referenced/has_many/proxy.rb
@@ -14,9 +14,55 @@ class HasMany
class Proxy < Association::Many
extend Forwardable
+ # Class-level methods for HasMany::Proxy
+ module ClassMethods
+
+ # Get the eager loader object for this type of association.
+ #
+ # @example Get the eager loader object
+ #
+ # @param [ Association ] association The association object.
+ # @param [ Array ] docs The array of documents.
+ #
+ # @return [ Mongoid::Association::Referenced::HasMany::Eager ]
+ # The eager loader.
+ def eager_loader(association, docs)
+ Eager.new(association, docs)
+ end
+
+ # Returns true if the association is an embedded one. In this case
+ # always false.
+ #
+ # @example Is this association embedded?
+ # Referenced::Many.embedded?
+ #
+ # @return [ false ] Always false.
+ def embedded?
+ false
+ end
+ end
+
+ extend ClassMethods
+
def_delegator :criteria, :count
def_delegators :_target, :first, :in_memory, :last, :reset, :uniq
+ # Instantiate a new references_many association. Will set the foreign key
+ # and the base on the inverse object.
+ #
+ # @example Create the new association.
+ # Referenced::Many.new(base, target, association)
+ #
+ # @param [ Mongoid::Document ] base The document this association hangs off of.
+ # @param [ Array ] target The target of the association.
+ # @param [ Mongoid::Association::Relatable ] association The association metadata.
+ def initialize(base, target, association)
+ enum = HasMany::Enumerable.new(target, base, association)
+ super(base, enum, association) do
+ raise_mixed if klass.embedded? && !klass.cyclic?
+ end
+ end
+
# Appends a document or array of documents to the association. Will set
# the parent and update the index in the process.
#
@@ -63,6 +109,7 @@ def concat(documents)
append(doc)
save_or_delay(doc, docs, inserts) if persistable?
end
+
persist_delayed(docs, inserts)
self
end
@@ -101,17 +148,22 @@ def build(attributes = {}, type = nil)
# @return [ Mongoid::Document ] The matching document.
def delete(document)
execute_callbacks_around(:remove, document) do
- _target.delete(document) do |doc|
+ result = _target.delete(document) do |doc|
if doc
unbind_one(doc)
cascade!(doc) unless _assigning?
end
- end.tap do
- reset_unloaded
end
+
+ reset_unloaded
+ result
end
end
+ # Mongoid::Extensions::Array defines Array#delete_one, so we need
+ # to make sure that method behaves reasonably on proxies, too.
+ alias_method :delete_one, :delete
+
# Deletes all related documents from the database given the supplied
# conditions.
#
@@ -220,22 +272,6 @@ def find(*args, &block)
matching
end
- # Instantiate a new references_many association. Will set the foreign key
- # and the base on the inverse object.
- #
- # @example Create the new association.
- # Referenced::Many.new(base, target, association)
- #
- # @param [ Mongoid::Document ] base The document this association hangs off of.
- # @param [ Array ] target The target of the association.
- # @param [ Mongoid::Association::Relatable ] association The association metadata.
- def initialize(base, target, association)
- enum = HasMany::Enumerable.new(target, base, association)
- init(base, enum, association) do
- raise_mixed if klass.embedded? && !klass.cyclic?
- end
- end
-
# Removes all associations between the base document and the target
# documents by deleting the foreign keys and the references, orphaning
# the target documents in the process.
@@ -260,25 +296,24 @@ def nullify
#
# @return [ Many ] The association emptied.
def purge
- if _association.destructive?
- after_remove_error = nil
- criteria.delete_all
- many = _target.clear do |doc|
- execute_callback :before_remove, doc
- unbind_one(doc)
- doc.destroyed = true
- begin
- execute_callback :after_remove, doc
- rescue StandardError => e
- after_remove_error = e
- end
- end
- raise after_remove_error if after_remove_error
+ return nullify unless _association.destructive?
- many
- else
- nullify
+ after_remove_error = nil
+ criteria.delete_all
+ many = _target.clear do |doc|
+ execute_callback :before_remove, doc
+ unbind_one(doc)
+ doc.destroyed = true
+ begin
+ execute_callback :after_remove, doc
+ rescue StandardError => e
+ after_remove_error = e
+ end
end
+
+ raise after_remove_error if after_remove_error
+
+ many
end
alias_method :clear, :purge
@@ -514,11 +549,9 @@ def remove_all(conditions = nil, method = :delete_all)
# @param [ Array