From 09e48c058aadef60d10af33d291a13260ddf8da5 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Mon, 11 May 2026 17:18:43 +0900 Subject: [PATCH] Default trackables to embedded relations only (v1.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Including Mongoid::RelationsDirtyTracking now tracks embeds_one and embeds_many only. Tracking referenced relations (has_one, has_many, has_and_belongs_to_many, belongs_to) requires opt-in via `relations_dirty_tracking track_referenced: true`. Tracking referenced relations forces those associations to be loaded on every after_initialize and after_save callback, which is a real cost when documents are read in bulk or via background jobs. The vast majority of callers only need embedded tracking, so this becomes the default and referenced tracking is opt-in. This is a breaking change → MAJOR version bump to 1.0.0. CHANGELOG, README upgrading guide, and tests for both the new default and the opt-in path are included. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 19 +++ README.md | 111 +++++++++++++++++- lib/mongoid/relations_dirty_tracking.rb | 30 +++-- .../relations_dirty_tracking/version.rb | 2 +- .../mongoid/relations_dirty_tracking_spec.rb | 49 ++++++++ spec/spec_helper.rb | 20 +++- 6 files changed, 218 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 088dce3..5e01e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## v1.0.0 - May 2026 + +### Breaking changes + +- **Default trackables narrowed to embedded relations only.** Including + `Mongoid::RelationsDirtyTracking` now tracks `embeds_one` and `embeds_many` + relations only. Tracking `has_one`, `has_many`, `has_and_belongs_to_many`, + and `belongs_to` requires opt-in via `relations_dirty_tracking track_referenced: true`. + This is a performance change — tracking referenced relations forces them to be + loaded on every `after_initialize` and `after_save` callback. See the + [Upgrading from 0.x](README.md#upgrading-from-0x) section of the README. + +### Features + +- Add `:track_referenced` option to `relations_dirty_tracking` to re-enable + tracking of referenced relations on a per-class basis. +- Expose `EMBEDDED_TRACKABLES` and `REFERENCED_TRACKABLES` constants for + applications that need to introspect or override the trackable set. + ## v0.3.0 - May 2025 - Support Mongoid 8.0, 8.1, and 9.0 (drop support for earlier versions) diff --git a/README.md b/README.md index 5a7c224..b43cbd4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ class SampleDocument include Mongoid::RelationsDirtyTracking embeds_one :foo - has_many :bars + embeds_many :bars field :title, type: String end @@ -39,9 +39,9 @@ doc.bars << Bar.new(title: 'bar') doc.title = 'New title' doc.relations_changed? # => true -doc.relation_changes # => {"foo" => [nil, {"_id"=>"524c35ad1ac1c23084000040", "title" => "foo"}], "bars" => [nil, [{"_id"=>"524c35ad1ac1c23084000083"}]]} +doc.relation_changes # => {"foo" => [nil, {"_id"=>"524c35ad1ac1c23084000040", "title" => "foo"}], "bars" => [[], [{"_id"=>"524c35ad1ac1c23084000083", "title"=>"bar"}]]} doc.changed_with_relations? # => true -doc.changes_with_relations # => {"title" => [nil, "New title"], "foo" => [nil, {"_id"=>"524c35ad1ac1c23084000040", "title" => "foo"}], "bars" => [nil, [{"_id"=>"524c35ad1ac1c23084000083"}]]} +doc.changes_with_relations # => {"title" => [nil, "New title"], "foo" => [nil, {"_id"=>"524c35ad1ac1c23084000040", "title" => "foo"}], "bars" => [[], [{"_id"=>"524c35ad1ac1c23084000083", "title"=>"bar"}]]} doc.save doc.relations_changed? # => false @@ -50,6 +50,33 @@ doc.changed_with_relations? # => false doc.changes_with_relations # => {} ``` +By default only **embedded** relations (`embeds_one`, `embeds_many`) are +tracked. To also track referenced relations (`has_one`, `has_many`, +`has_and_belongs_to_many`, `belongs_to`), opt in per class: + +```ruby +class SampleDocument + include Mongoid::Document + include Mongoid::RelationsDirtyTracking + + embeds_many :tags + has_many :comments + belongs_to :author + + relations_dirty_tracking track_referenced: true +end +``` + +You can still narrow tracking further with `:only` and `:except`. These +filters are applied **within** the set of trackable relations selected by +`:track_referenced`: + +```ruby +relations_dirty_tracking only: %i[tags] # embedded only, only :tags +relations_dirty_tracking only: %i[tags comments], track_referenced: true +relations_dirty_tracking except: %i[tags], track_referenced: true # everything except :tags +``` + ## Disablement Relations dirty tracking can be resource intensive, @@ -91,6 +118,84 @@ Mongoid::RelationsDirtyTracking.disable do end ``` +## Upgrading from 0.x + +**v1.0.0 narrowed the default trackables to embedded relations only.** Earlier +versions tracked every kind of relation (embedded, has_one, has_many, HABTM, +belongs_to) by default. We changed the default because tracking referenced +relations forces those associations to be loaded on every `after_initialize` +and `after_save` callback — a significant cost when documents are read in bulk +or via background jobs. + +### How to tell if you are affected + +You are affected if any class in your application includes +`Mongoid::RelationsDirtyTracking` **and** relies on dirty tracking for +`has_one`, `has_many`, `has_and_belongs_to_many`, or `belongs_to` associations. +A class is *not* affected if it only uses dirty tracking for `embeds_one` / +`embeds_many` associations. + +A quick way to audit your codebase: + +```bash +grep -rln "include Mongoid::RelationsDirtyTracking" app/ lib/ +``` + +For each match, check whether the class actually consumes `relation_changes` / +`changes_with_relations` / `relations_changed?` for a referenced relation +(typically read by audit logs, `mongoid-history` callbacks, push-sync hooks, +etc.). + +### Restoring the v0.x behaviour + +If you were depending on referenced relation tracking, add +`track_referenced: true` to the macro call: + +```ruby +# Before (v0.x — tracked everything by default) +class Order + include Mongoid::Document + include Mongoid::RelationsDirtyTracking + + embeds_many :items + has_many :payments +end + +# After (v1.0 — restore previous behaviour for this class) +class Order + include Mongoid::Document + include Mongoid::RelationsDirtyTracking + + embeds_many :items + has_many :payments + + relations_dirty_tracking track_referenced: true +end +``` + +If you already call `relations_dirty_tracking` with `:only` or `:except`, add +the option to that call: + +```ruby +# Before (v0.x) +relations_dirty_tracking only: %i[items payments] + +# After (v1.0) +relations_dirty_tracking only: %i[items payments], track_referenced: true +``` + +### When you can leave the new default + +Most callers can leave the new default and benefit from the speedup. Common +cases where referenced tracking is *not* needed: + +- You only track embedded sub-documents (e.g. line items, addresses). +- You use the gem to invalidate a cache or trigger a webhook when an embedded + collection changes. +- Your referenced relations are also tracked through their own + `mongoid-history` / audit hooks on the **referenced** side, making the + parent-side tracking redundant. + ## Contributing 1. Fork it diff --git a/lib/mongoid/relations_dirty_tracking.rb b/lib/mongoid/relations_dirty_tracking.rb index ed0c963..cc674b0 100644 --- a/lib/mongoid/relations_dirty_tracking.rb +++ b/lib/mongoid/relations_dirty_tracking.rb @@ -7,6 +7,18 @@ module Mongoid module RelationsDirtyTracking extend ActiveSupport::Concern + EMBEDDED_TRACKABLES = [ + Mongoid::Association::Embedded::EmbedsOne::Proxy, + Mongoid::Association::Embedded::EmbedsMany::Proxy + ].freeze + + REFERENCED_TRACKABLES = [ + Mongoid::Association::Referenced::HasOne::Proxy, + Mongoid::Association::Referenced::HasMany::Proxy, + Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy, + Mongoid::Association::Referenced::BelongsTo::Proxy + ].freeze + class << self DISABLED_KEY = 'mongoid/relations_dirty_tracking/disabled' @@ -32,9 +44,17 @@ def thread_store end module ClassMethods + # Configure relations dirty tracking for this class. + # + # @option options [Array] :only Track only the listed relations. + # @option options [Array] :except Skip the listed relations. + # @option options [Boolean] :track_referenced Also track referenced relations + # (has_one, has_many, has_and_belongs_to_many, belongs_to). Defaults to + # `false` — only embedded relations are tracked by default for performance. def relations_dirty_tracking(options = {}) relations_dirty_tracking_options[:only] += [options[:only] || []].flatten.map(&:to_s) relations_dirty_tracking_options[:except] += [options[:except] || []].flatten.map(&:to_s) + relations_dirty_tracking_options[:track_referenced] = true if options[:track_referenced] end def track_relation?(rel_name) @@ -43,12 +63,8 @@ def track_relation?(rel_name) to_track = (!options[:only].blank? && options[:only].include?(rel_name)) || (options[:only].blank? && !options[:except].include?(rel_name)) - trackables = [Mongoid::Association::Embedded::EmbedsOne::Proxy, - Mongoid::Association::Embedded::EmbedsMany::Proxy, - Mongoid::Association::Referenced::HasOne::Proxy, - Mongoid::Association::Referenced::HasMany::Proxy, - Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy, - Mongoid::Association::Referenced::BelongsTo::Proxy] + trackables = EMBEDDED_TRACKABLES + trackables += REFERENCED_TRACKABLES if options[:track_referenced] to_track && trackables.include?(relations[rel_name].try(:relation)) end @@ -63,7 +79,7 @@ def tracked_relations after_save :store_relations_shadow cattr_accessor :relations_dirty_tracking_options - self.relations_dirty_tracking_options = { only: [], except: ['versions'] } + self.relations_dirty_tracking_options = { only: [], except: ['versions'], track_referenced: false } def store_relations_shadow @relations_shadow = {} diff --git a/lib/mongoid/relations_dirty_tracking/version.rb b/lib/mongoid/relations_dirty_tracking/version.rb index 36f14ea..1ff5bac 100644 --- a/lib/mongoid/relations_dirty_tracking/version.rb +++ b/lib/mongoid/relations_dirty_tracking/version.rb @@ -2,6 +2,6 @@ module Mongoid module RelationsDirtyTracking - VERSION = '0.3.0' + VERSION = '1.0.0' end end diff --git a/spec/lib/mongoid/relations_dirty_tracking_spec.rb b/spec/lib/mongoid/relations_dirty_tracking_spec.rb index 320499b..9573a58 100644 --- a/spec/lib/mongoid/relations_dirty_tracking_spec.rb +++ b/spec/lib/mongoid/relations_dirty_tracking_spec.rb @@ -348,6 +348,55 @@ expect(TestDocumentWithExceptOption.track_relation?('one_related')).to be true end end + + context 'with the v1.0 default (no track_referenced option)' do + it 'tracks embedded relations' do + expect(TestDocumentDefault.track_relation?(:many_documents)).to be true + end + + it 'does not track has_one relations' do + expect(TestDocumentDefault.track_relation?(:one_related)).to be false + end + + it 'does not track has_many relations' do + expect(TestDocumentDefault.track_relation?(:many_related)).to be false + end + end + end + + describe 'v1.0 default behaviour — embedded only' do + describe '.tracked_relations' do + subject { TestDocumentDefault.tracked_relations } + + it { is_expected.to include('many_documents') } + it { is_expected.to_not include('one_related') } + it { is_expected.to_not include('many_related') } + end + + describe '#relations_dirty_tracking_options' do + it 'defaults track_referenced to false' do + expect(TestDocumentDefault.relations_dirty_tracking_options[:track_referenced]).to be false + end + + it 'is true when the macro was called with track_referenced: true' do + expect(TestDocument.relations_dirty_tracking_options[:track_referenced]).to be true + end + end + + context 'when an embedded document is added' do + subject(:doc) { TestDocumentDefault.create } + + before do + @embedded_doc = TestEmbeddedDocument.new + doc.many_documents << @embedded_doc + end + + its(:relations_changed?) { is_expected.to be true } + + it 'reports the embedded change' do + expect(doc.relation_changes['many_documents']).to eq([[], [@embedded_doc.attributes]]) + end + end end describe 'by default the versions relation is not tracked' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9d2add1..1ac3e2e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,7 +28,8 @@ end Mongoid.configure do |config| - config.connect_to('mongoid_relations_dirty_tracking_test') + hosts = ENV['MONGODB_HOSTS']&.split(',') || %w[localhost:27017] + config.clients.default = { hosts: hosts, database: 'mongoid_relations_dirty_tracking_test' } config.belongs_to_required_by_default = false end @@ -42,6 +43,9 @@ class TestDocument has_one :one_related, class_name: 'TestRelatedDocument' has_many :many_related, class_name: 'TestRelatedDocument' has_and_belongs_to_many :many_to_many_related, class_name: 'TestRelatedDocument' + + # Tests cover both embedded and referenced behaviour, so opt in. + relations_dirty_tracking track_referenced: true end class TestEmbeddedDocument @@ -59,6 +63,8 @@ class TestRelatedDocument belongs_to :test_document, inverse_of: :one_related field :title, type: String + + relations_dirty_tracking track_referenced: true end class TestDocumentWithOnlyOption @@ -78,5 +84,15 @@ class TestDocumentWithExceptOption embeds_many :many_documents, class_name: 'TestEmbeddedDocument' has_one :one_related, class_name: 'TestRelatedDocument' - relations_dirty_tracking except: 'many_documents' + relations_dirty_tracking except: 'many_documents', track_referenced: true +end + +# Default behaviour as of v1.0.0: embedded relations tracked, referenced ignored. +class TestDocumentDefault + include Mongoid::Document + include Mongoid::RelationsDirtyTracking + + embeds_many :many_documents, class_name: 'TestEmbeddedDocument' + has_one :one_related, class_name: 'TestRelatedDocument' + has_many :many_related, class_name: 'TestRelatedDocument' end