Skip to content

Commit 82ab326

Browse files
committed
[+] add config option to disable the feature
1 parent 5903ab9 commit 82ab326

File tree

6 files changed

+119
-6
lines changed

6 files changed

+119
-6
lines changed

lib/mongoid/attributes.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def remove_attribute(name)
163163
#
164164
# @api private
165165
def clear_demongoized_cache(name)
166-
@__demongoized_cache.delete(name)
166+
@__demongoized_cache.delete(name) if Mongoid::Config.cache_attribute_values?
167167
end
168168
private :clear_demongoized_cache
169169

@@ -262,7 +262,7 @@ def write_attributes(attrs = nil)
262262
# it from the database with missing fields.
263263
#
264264
# Cache the projector keyed by __selected_fields to automatically handle
265-
# invalidation when selected fields change.
265+
# invalidation when selected fields change (only if caching is enabled).
266266
#
267267
# @example Is the attribute missing?
268268
# document.attribute_missing?("test")
@@ -271,10 +271,14 @@ def write_attributes(attrs = nil)
271271
#
272272
# @return [ true | false ] If the attribute is missing.
273273
def attribute_missing?(name)
274-
projector = @__projector_cache.compute_if_absent(__selected_fields) do
275-
Projector.new(__selected_fields)
274+
if Mongoid::Config.cache_attribute_values?
275+
projector = @__projector_cache.compute_if_absent(__selected_fields) do
276+
Projector.new(__selected_fields)
277+
end
278+
!projector.attribute_or_path_allowed?(name)
279+
else
280+
!Projector.new(__selected_fields).attribute_or_path_allowed?(name)
276281
end
277-
!projector.attribute_or_path_allowed?(name)
278282
end
279283

280284
# Return type-casted attributes.

lib/mongoid/config.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,23 @@ def validate_isolation_level!(level)
217217
# document might be ignored, or it might work, depending on the situation.
218218
option :immutable_ids, default: true
219219

220+
# When this flag is true, Mongoid will cache demongoized attribute values
221+
# to improve read performance. The cache stores both the raw BSON value
222+
# and the demongoized Ruby object, automatically invalidating when the
223+
# underlying raw value changes.
224+
#
225+
# This optimization can significantly improve performance for fields with
226+
# expensive demongoization (e.g., Time, Date, custom types), especially
227+
# in read-heavy workloads.
228+
#
229+
# The cache is disabled by default to maintain backward compatibility.
230+
# Enable it to gain performance improvements:
231+
#
232+
# Mongoid.configure do |config|
233+
# config.cache_attribute_values = true
234+
# end
235+
option :cache_attribute_values, default: false
236+
220237
# When this flag is true, callbacks for every embedded document will be
221238
# called only once, even if the embedded document is embedded in multiple
222239
# documents in the root document's dependencies graph.

lib/mongoid/document.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ def prepare_to_process_attributes
253253
#
254254
# @api private
255255
def initialize_field_caches
256+
return unless Mongoid::Config.cache_attribute_values?
256257
@__projector_cache = Concurrent::Map.new
257258
@__demongoized_cache = Concurrent::Map.new
258259
end

lib/mongoid/fields.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,8 @@ def create_field_getter(name, meth, field)
693693
# Don't cache localized fields as they depend on I18n.locale
694694
elsif field.localized?
695695
process_raw_attribute(name.to_s, raw, field)
696-
else
696+
# Check if caching is enabled
697+
elsif Mongoid::Config.cache_attribute_values?
697698
# Atomically fetch or compute the cached value
698699
# Cache stores [raw_value, demongoized_value] to detect stale cache
699700
value = @__demongoized_cache.compute_if_absent(name) do
@@ -719,6 +720,9 @@ def create_field_getter(name, meth, field)
719720
attribute_will_change!(name.to_s) if demongoized_value.resizable? && !is_relation
720721

721722
demongoized_value
723+
else
724+
# Caching disabled - use original behavior
725+
process_raw_attribute(name.to_s, raw, field)
722726
end
723727
end
724728
end

spec/mongoid/config_spec.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,84 @@
846846
end
847847
end
848848

849+
describe 'cache_attribute_values option' do
850+
context 'when not set in the config' do
851+
it 'defaults to false' do
852+
Mongoid::Config.reset
853+
configuration = CONFIG.merge(options: {})
854+
855+
Mongoid.configure { |config| config.load_configuration(configuration) }
856+
857+
expect(Mongoid::Config.cache_attribute_values).to be(false)
858+
end
859+
end
860+
861+
context 'when set to true in the config' do
862+
it 'enables field value caching' do
863+
Mongoid::Config.reset
864+
configuration = CONFIG.merge(options: { cache_attribute_values: true })
865+
866+
Mongoid.configure { |config| config.load_configuration(configuration) }
867+
868+
expect(Mongoid::Config.cache_attribute_values).to be(true)
869+
end
870+
end
871+
872+
context 'when set to false in the config' do
873+
it 'disables field value caching' do
874+
Mongoid::Config.reset
875+
configuration = CONFIG.merge(options: { cache_attribute_values: false })
876+
877+
Mongoid.configure { |config| config.load_configuration(configuration) }
878+
879+
expect(Mongoid::Config.cache_attribute_values).to be(false)
880+
end
881+
end
882+
883+
context 'functional behavior' do
884+
let(:band_class) do
885+
Class.new do
886+
include Mongoid::Document
887+
store_in collection: 'bands'
888+
field :name, type: String
889+
field :updated, type: Time
890+
end
891+
end
892+
893+
before do
894+
stub_const('CacheBand', band_class)
895+
end
896+
897+
it 'uses caching when enabled' do
898+
Mongoid::Config.cache_attribute_values = true
899+
900+
band = CacheBand.new(name: 'Test', updated: Time.current)
901+
902+
# First access should populate cache
903+
first_result = band.updated
904+
905+
# Second access should return cached value (same object_id if caching works)
906+
second_result = band.updated
907+
908+
expect(first_result.object_id).to eq(second_result.object_id)
909+
end
910+
911+
it 'does not use caching when disabled' do
912+
Mongoid::Config.cache_attribute_values = false
913+
914+
band = CacheBand.new(name: 'Test', updated: Time.current)
915+
916+
# Each access should call process_raw_attribute
917+
first_result = band.updated
918+
second_result = band.updated
919+
920+
# When caching is disabled, cache objects should not be initialized
921+
expect(band.instance_variable_get(:@__demongoized_cache)).to be_nil
922+
expect(band.instance_variable_get(:@__projector_cache)).to be_nil
923+
end
924+
end
925+
end
926+
849927
describe 'deprecations' do
850928
{}.each do |option, default|
851929

spec/mongoid/fields/performance_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
# Core field types (String, Integer, Float, etc.) must have exactly 0 allocations
1717
# to verify the caching optimization is working correctly.
1818
describe 'Mongoid::Fields performance optimizations' do
19+
# Enable caching for all performance tests
20+
around do |example|
21+
original_value = Mongoid::Config.cache_attribute_values
22+
Mongoid::Config.cache_attribute_values = true
23+
example.run
24+
ensure
25+
Mongoid::Config.cache_attribute_values = original_value
26+
end
27+
1928
let(:band) do
2029
Band.new(
2130
name: 'Test Band',

0 commit comments

Comments
 (0)