Skip to content

Commit 21389fe

Browse files
authored
MONGOID-5404 Populate attributes_before_type_cast on read from database (#5347)
* MONGOID-5222 update attributes_before_typecast on read * MONGOID-5222 fix test * MONGOID-5404 change abtc to function like rails * MONGOID-5404 add a release note * MONGOID-5404 update tests * MONGOID-5404 fix docs
1 parent 86a4a13 commit 21389fe

File tree

9 files changed

+206
-11
lines changed

9 files changed

+206
-11
lines changed

docs/release-notes/mongoid-8.0.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,3 +600,26 @@ returns ``true``:
600600
b.name == "Depeche Mode"
601601
end
602602
# => #<Band _id: 62c58e383282a4cbe82bd74b, name: "Depeche Mode">
603+
604+
605+
Changes to the ``attributes_before_type_cast`` Hash
606+
---------------------------------------------------
607+
608+
The ``attributes_before_type_cast`` hash has been changed to function more like
609+
ActiveRecord:
610+
611+
- On instantiation of a new model (without parameters), the
612+
``attributes_before_type_cast`` hash has the same contents as the
613+
``attributes`` hash. If parameters are passed to the initializer, those
614+
values will be stored in the ``attributes_before_type_cast`` hash before
615+
they are ``mongoized``.
616+
- When assigning a value to the model, the ``mongoized`` value (i.e. when
617+
assiging '1' to an Integer field, it is ``mongoized`` to 1) is stored in
618+
the ``attributes`` hash, whereas the raw value (i.e. '1') is stored in the
619+
``attributes_before_type_cast`` hash.
620+
- When saving, creating (i.e. using the ``create!`` method), or reloading the
621+
model, the ``attributes_before_type_cast`` hash is reset to have the same
622+
contents as the ``attributes`` hash.
623+
- When reading a document from the database, the ``attributes_before_type_cast``
624+
hash contains the attributes as they appear in the database, as opposed to
625+
their ``demongoized`` form.

lib/mongoid/attributes.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,25 @@ def has_attribute_before_type_cast?(name)
8686
def read_attribute(name)
8787
field = fields[name.to_s]
8888
raw = read_raw_attribute(name)
89-
# Keep this code consistent with Mongoid::Fields.create_field_getter
89+
process_raw_attribute(name.to_s, raw, field)
90+
end
91+
alias :[] :read_attribute
92+
93+
94+
# Process the raw attribute values just read from the documents attributes.
95+
#
96+
# @param [ String ] name The name of the attribute to get.
97+
# @param [ Object ] raw The raw attribute value.
98+
# @param [ Field | nil ] field The field to use for demongoization or nil.
99+
#
100+
# @return [ Object ] The value of the attribute.
101+
#
102+
# @api private
103+
def process_raw_attribute(name, raw, field)
90104
value = field ? field.demongoize(raw) : raw
91-
attribute_will_change!(name.to_s) if value.resizable?
105+
attribute_will_change!(name) if value.resizable?
92106
value
93107
end
94-
alias :[] :read_attribute
95108

96109
# Read a value from the attributes before type cast. If the value has not
97110
# yet been assigned then this will return the attribute's existing value

lib/mongoid/changeable.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def move_changes
8282
# document.post_persist
8383
def post_persist
8484
reset_persisted_descendants
85+
reset_attributes_before_type_cast
8586
move_changes
8687
end
8788

@@ -248,6 +249,10 @@ def reset_attribute_to_default!(attr)
248249
end
249250
end
250251

252+
def reset_attributes_before_type_cast
253+
@attributes_before_type_cast = @attributes.dup
254+
end
255+
251256
module ClassMethods
252257

253258
private

lib/mongoid/document.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def construct_document(attrs = nil, execute_callbacks: true)
224224
process_attributes(attrs) do
225225
yield(self) if block_given?
226226
end
227+
@attributes_before_type_cast = @attributes.merge(attributes_before_type_cast)
227228

228229
if execute_callbacks
229230
apply_post_processed_defaults
@@ -318,6 +319,10 @@ def instantiate_document(attrs = nil, selected_fields = nil, execute_callbacks:
318319
doc = allocate
319320
doc.__selected_fields = selected_fields
320321
doc.instance_variable_set(:@attributes, attributes)
322+
# TODO: remove the to_h when the legacy_attributes flag is removed.
323+
# The to_h ensures that we don't accidentally make attributes_before_type_cast
324+
# a BSON::Document.
325+
doc.instance_variable_set(:@attributes_before_type_cast, attributes&.to_h.dup)
321326

322327
if execute_callbacks
323328
doc.apply_defaults

lib/mongoid/fields.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -636,10 +636,7 @@ def create_field_getter(name, meth, field)
636636
if lazy_settable?(field, raw)
637637
write_attribute(name, field.eval_default(self))
638638
else
639-
# Keep this code consistent with Mongoid::Attributes#read_attribute
640-
value = field.demongoize(raw)
641-
attribute_will_change!(name) if value.resizable?
642-
value
639+
process_raw_attribute(name.to_s, raw, field)
643640
end
644641
end
645642
end

lib/mongoid/reloadable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def reload
2727
raise Errors::DocumentNotFound.new(self.class, _id, shard_keys)
2828
end
2929
@attributes = reloaded
30-
@attributes_before_type_cast = {}
30+
@attributes_before_type_cast = @attributes.dup
3131
@changed_attributes = {}
3232
@previous_changes = {}
3333
@previous_attributes = {}

spec/mongoid/attributes_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,8 +1266,8 @@
12661266

12671267
context "before the attribute has been assigned" do
12681268

1269-
it "returns false" do
1270-
expect(person.has_attribute_before_type_cast?(:age)).to be false
1269+
it "returns true" do
1270+
expect(person.has_attribute_before_type_cast?(:age)).to be true
12711271
end
12721272
end
12731273

spec/mongoid/fields_spec.rb

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,142 @@
588588
expect(person.age_before_type_cast).to eq("old")
589589
end
590590
end
591+
592+
context "when reloading" do
593+
594+
let(:product) do
595+
Product.create!(price: '1')
596+
end
597+
598+
before do
599+
product.reload
600+
end
601+
602+
it "resets the attributes_before_type_cast to the attributes hash" do
603+
expect(product.attributes_before_type_cast).to eq(product.attributes)
604+
end
605+
606+
it "the *_before_type_cast method returns the demongoized value" do
607+
expect(product.price_before_type_cast).to eq(1)
608+
end
609+
end
610+
611+
context "when reloading and writing a demongoizable value" do
612+
613+
let(:product) do
614+
Product.create!.tap do |product|
615+
Product.collection.update_one({ _id: product.id }, { :$set => { price: '1' }})
616+
end
617+
end
618+
619+
before do
620+
product.reload
621+
end
622+
623+
it "resets the attributes_before_type_cast to the attributes hash" do
624+
expect(product.attributes_before_type_cast).to eq(product.attributes)
625+
end
626+
627+
it "the *_before_type_cast method returns the mongoized value" do
628+
expect(product.price_before_type_cast).to eq('1')
629+
end
630+
end
631+
632+
context "when reading from the db" do
633+
634+
let(:product) do
635+
Product.create!(price: '1')
636+
end
637+
638+
let(:from_db) do
639+
Product.find(product.id)
640+
end
641+
642+
it "resets the attributes_before_type_cast to the attributes hash" do
643+
expect(from_db.attributes_before_type_cast).to eq(from_db.attributes)
644+
end
645+
646+
it "the *_before_type_cast method returns the demongoized value" do
647+
expect(from_db.price_before_type_cast).to eq(1)
648+
end
649+
end
650+
651+
context "when reading from the db after writing a demongoizable value" do
652+
653+
let(:product) do
654+
Product.create!.tap do |product|
655+
Product.collection.update_one({ _id: product.id }, { :$set => { price: '1' }})
656+
end
657+
end
658+
659+
let(:from_db) do
660+
Product.find(product.id)
661+
end
662+
663+
it "resets the attributes_before_type_cast to the attributes hash" do
664+
expect(from_db.attributes_before_type_cast).to eq(from_db.attributes)
665+
end
666+
667+
it "the *_before_type_cast method returns the mongoized value" do
668+
expect(from_db.price_before_type_cast).to eq('1')
669+
end
670+
end
671+
672+
context "when making a new model" do
673+
674+
context "when using new with no options" do
675+
let(:product) { Product.new }
676+
677+
it "sets the attributes_before_type_cast to the attributes hash" do
678+
expect(product.attributes_before_type_cast).to eq(product.attributes)
679+
end
680+
end
681+
682+
context "when using new with options" do
683+
let(:product) { Product.new(price: '1') }
684+
685+
let(:abtc) do
686+
product.attributes.merge('price' => '1')
687+
end
688+
689+
it "has the attributes before type cast" do
690+
expect(product.attributes_before_type_cast).to eq(abtc)
691+
end
692+
end
693+
694+
context "when persisting the model" do
695+
let(:product) { Product.new(price: '1') }
696+
697+
let(:abtc) do
698+
product.attributes.merge('price' => '1')
699+
end
700+
701+
before do
702+
expect(product.attributes_before_type_cast).to eq(abtc)
703+
product.save!
704+
end
705+
706+
it "resets the attributes_before_type_cast to the attributes" do
707+
expect(product.attributes_before_type_cast).to eq(product.attributes)
708+
end
709+
end
710+
711+
context "when using create! without options" do
712+
let(:product) { Product.create! }
713+
714+
it "resets the attributes_before_type_cast to the attributes" do
715+
expect(product.attributes_before_type_cast).to eq(product.attributes)
716+
end
717+
end
718+
719+
context "when using create! with options" do
720+
let(:product) { Product.create!(price: '1') }
721+
722+
it "resets the attributes_before_type_cast to the attributes" do
723+
expect(product.attributes_before_type_cast).to eq(product.attributes)
724+
end
725+
end
726+
end
591727
end
592728

593729
describe "#setter=" do
@@ -741,6 +877,22 @@
741877
end
742878
end
743879
end
880+
881+
context "when the field needs to be mongoized" do
882+
883+
before do
884+
product.price = "1"
885+
product.save!
886+
end
887+
888+
it "mongoizes the value" do
889+
expect(product.price).to eq(1)
890+
end
891+
892+
it "stores the value in the mongoized form" do
893+
expect(product.attributes_before_type_cast["price"]).to eq(1)
894+
end
895+
end
744896
end
745897

746898
describe "#defaults" do

spec/mongoid/reloadable_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
end
104104

105105
it "resets attributes_before_type_cast" do
106-
expect(person.attributes_before_type_cast).to be_empty
106+
expect(person.attributes_before_type_cast).to eq(person.attributes)
107107
end
108108
end
109109

0 commit comments

Comments
 (0)