Skip to content

Commit 3a7cd7c

Browse files
authored
MONGOID-5213 Document changes to BigDecimal type and addition of global flag (#5126)
* MONGOID-5213 add global flag to config docs * MONGOID-5213 add documentation for BigDecimal fields * MONGOID-5213 add docs for storing BigDecimals in dynamic fields * MONGOID-5213 change 'under the hood' * MONGOID-5213 add documentation for migration to decimal128 backed BigDecimal fields * MONGOID-5213 change number of options * MONGOID-5213 update language to stress conversion before setting config * MONGOID-5213 make sure to say major version
1 parent d213977 commit 3a7cd7c

File tree

2 files changed

+120
-16
lines changed

2 files changed

+120
-16
lines changed

source/reference/configuration.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ for details on driver options.
280280
# Ruby on Rails logger instance. (default: :info)
281281
log_level: :info
282282

283+
# When using the BigDecimal field type, store the value in the database
284+
# as a BSON::Decimal128 instead of a string. (default: false)
285+
map_big_decimal_to_decimal128: false
286+
283287
# Preload all models in development, needed when models use
284288
# inheritance. (default: false)
285289
preload_models: false

source/reference/fields.txt

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ You can safely omit type specifications when:
109109

110110
Types that are not supported as dynamic attributes since they cannot be cast are:
111111

112-
- ``BigDecimal``
113112
- ``Date``
114113
- ``DateTime``
115114
- ``Range``
@@ -247,10 +246,10 @@ assignment to a ``Time`` field:
247246

248247
class Voter
249248
include Mongoid::Document
250-
249+
251250
field :registered_at, type: Time
252251
end
253-
252+
254253
Voter.new(registered_at: Date.today)
255254
# => #<Voter _id: 5fdd80392c97a618f07ba344, registered_at: 2020-12-18 05:00:00 UTC>
256255

@@ -416,6 +415,107 @@ matches strings containing "hello" before a newline, besides strings ending in
416415
This is because the meaning of ``$`` is different between PCRE and Ruby
417416
regular expressions.
418417

418+
BigDecimal Fields
419+
-----------------
420+
421+
The ``BigDecimal`` field type is used to store numbers with increased precision.
422+
423+
The ``BigDecimal`` field type stores its values in two different ways in the
424+
database, depending on the value of the ``Mongoid.map_big_decimal_to_decimal128``
425+
global config option. If this flag is set to false (which is the default),
426+
the ``BigDecimal`` field will be stored as a string, otherwise it will be stored
427+
as a ``BSON::Decimal128``.
428+
429+
The ``BigDecimal`` field type has some limitations when converting to and from
430+
a ``BSON::Decimal128``:
431+
432+
- ``BSON::Decimal128`` has a limited range and precision, while ``BigDecimal``
433+
has no restrictions in terms of range and precision. ``BSON::Decimal128`` has
434+
a max value of approximately ``10^6145`` and a min value of approximately
435+
``-10^6145``, and has a maximum of 34 bits of precision. When attempting to
436+
store values that don't fit into a ``BSON::Decimal128``, it is recommended to
437+
have them stored as a string instead of a ``BSON::Decimal128``. You can do
438+
that by setting ``Mongoid.map_big_decimal_to_decimal128`` to ``false``. If a
439+
value that does not fit in a ``BSON::Decimal128`` is attempted to be stored
440+
as one, an error will be raised.
441+
442+
- ``BSON::Decimal128`` is able to accept signed ``NaN`` values, while
443+
``BigDecimal`` is not. When retrieving signed ``NaN`` values from
444+
the database using the ``BigDecimal`` field type, the ``NaN`` will be
445+
unsigned.
446+
447+
- ``BSON::Decimal128`` maintains trailing zeroes when stored in the database.
448+
``BigDecimal``, however, does not maintain trailing zeroes, and therefore
449+
retrieving ``BSON::Decimal128`` values using the ``BigDecimal`` field type
450+
may result in a loss of precision.
451+
452+
There is an additional caveat when storing a ``BigDecimal`` in a field with no
453+
type (i.e. a dynamically typed field) and ``Mongoid.map_big_decimal_to_decimal128``
454+
is ``false``. In this case, the ``BigDecimal`` is stored as a string, and since a
455+
dynamic field is being used, querying for that field with a ``BigDecimal`` will
456+
not find the string for that ``BigDecimal``, since the query is looking for a
457+
``BigDecimal``. In order to query for that string, the ``BigDecimal`` must
458+
first be converted to a string with ``to_s``. Note that this is not a problem
459+
when the field has type ``BigDecimal``.
460+
461+
If you wish to avoid using ``BigDecimal`` altogether, you can set the field
462+
type to ``BSON::Decimal128``. This will allow you to keep track of trailing
463+
zeroes and signed ``NaN`` values.
464+
465+
Migration to ``decimal128``-backed ``BigDecimal`` Field
466+
```````````````````````````````````````````````````````
467+
In a future major version of Mongoid, the ``Mongoid.map_big_decimal_to_decimal128``
468+
global config option will be defaulted to ``true``. When this flag is turned on,
469+
``BigDecimal`` values in queries will not match to the strings that are already
470+
stored in the database; they will only match to ``decimal128`` values that are
471+
in the database. If you have a ``BigDecimal`` field that is backed by strings,
472+
you have three options:
473+
474+
1. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
475+
set to ``false``, and you can continue storing your ``BigDecimal`` values as
476+
strings. Note that you are surrendering the advantages of storing ``BigDecimal``
477+
values as a ``decimal128``, like being able to do queries and aggregations
478+
based on the numerical value of the field.
479+
480+
2. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
481+
set to ``true``, and you can convert all values for that field from strings to
482+
``decimal128`` values in the database. You should do this conversion before
483+
setting the global config option to true. An example query to accomplish this
484+
is as follows:
485+
486+
.. code-block:: javascript
487+
488+
db.bands.updateMany({
489+
"field": { "$exists": true }
490+
}, [
491+
{
492+
"$set": {
493+
"field": { "$toDecimal": "$field" }
494+
}
495+
}
496+
])
497+
498+
This query updates all documents that have the given field, setting that
499+
field to its corresponding ``decimal128`` value. Note that this query only
500+
works in MongoDB 4.2+.
501+
502+
3. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
503+
set to ``true``, and you can have both strings and ``decimal128`` values for
504+
that field. This way, only ``decimal128`` values will be inserted into and
505+
updated to the database going forward. Note that you still don't get the
506+
full advantages of using only ``decimal128`` values, but your dataset is
507+
slowly migrating to all ``decimal128`` values, as old string values are
508+
updated to ``decimal128`` and new ``decimal128`` values are added. With this
509+
setup, you can still query for ``BigDecimal`` values as follows:
510+
511+
.. code-block:: ruby
512+
513+
Mongoid.map_big_decimal_to_decimal128 = true
514+
big_decimal = BigDecimal('2E9')
515+
Band.in(sales: [big_decimal, big_decimal.to_s]).to_a
516+
517+
This query will find all values that are either a ``decimal128`` value or
518+
a string that match that value.
419519

420520
.. _field-default-values:
421521

@@ -524,17 +624,17 @@ from the aliased field:
524624

525625
class Band
526626
include Mongoid::Document
527-
627+
528628
field :name, type: String
529629
alias_attribute :n, :name
530630
end
531-
631+
532632
band = Band.new(n: 'Astral Projection')
533633
# => #<Band _id: 5fc1c1ee2c97a64accbeb5e1, name: "Astral Projection">
534-
634+
535635
band.attributes
536636
# => {"_id"=>BSON::ObjectId('5fc1c1ee2c97a64accbeb5e1'), "name"=>"Astral Projection"}
537-
637+
538638
band.n
539639
# => "Astral Projection"
540640

@@ -559,11 +659,11 @@ This is useful for storing different values in ``id`` and ``_id`` fields:
559659

560660
class Band
561661
include Mongoid::Document
562-
662+
563663
unalias_attribute :id
564664
field :id, type: String
565665
end
566-
666+
567667
Band.new(id: '42')
568668
# => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">
569669

@@ -691,19 +791,19 @@ getter as follows:
691791

692792
class DistanceMeasurement
693793
include Mongoid::Document
694-
794+
695795
field :value, type: Float
696796
field :unit, type: String
697-
797+
698798
def unit
699799
read_attribute(:unit) || "m"
700800
end
701-
801+
702802
def to_s
703803
"#{value} #{unit}"
704804
end
705805
end
706-
806+
707807
measurement = DistanceMeasurement.new(value: 2)
708808
measurement.to_s
709809
# => "2.0 m"
@@ -717,18 +817,18 @@ may be implemented as follows:
717817

718818
class DistanceMeasurement
719819
include Mongoid::Document
720-
820+
721821
field :value, type: Float
722822
field :unit, type: String
723-
823+
724824
def unit=(value)
725825
if value.blank?
726826
value = nil
727827
end
728828
write_attribute(:unit, value)
729829
end
730830
end
731-
831+
732832
measurement = DistanceMeasurement.new(value: 2, unit: "")
733833
measurement.attributes
734834
# => {"_id"=>BSON::ObjectId('613fa15aa15d5d617216104c'), "value"=>2.0, "unit"=>nil}

0 commit comments

Comments
 (0)