对货币数据进行建模
Overview
处理货币数据的应用程序通常需要能够捕获货币的小数单位,并且需要在执行算术时以精确的精度模拟小数舍入。许多现代系统使用的基于二进制的浮点运算(即,float、double)无法表示精确的小数部分,需要一定程度的近似,因此不适合用于货币运算。在对货币数据进行建模时,此约束是一个重要的考虑因素。
可采用多种使用数值和非数值模型的方法对 MongoDB 中的货币数据进行建模。
数值模型
如果您需要在数据库中查询精确、数学上有效的匹配项或需要执行服务器端算术,则数值模型可能比较合适,例如 $inc
、$mul
和聚合管道算术。
以下方法遵循数值模型:
使用十进制 BSON 类型,这是一种基于十进制的浮点格式,能够提供精确的精度。
使用比例因子,通过将货币值乘以比例因子 10 的倍数将货币值转换为 64 位整数(
long
BSON 类型)。
Non-Numeric Model
如果不需要对货币数据执行服务器端运算,或者如果服务器端采用近似值就足够,那么使用非数字模型对货币数据进行建模可能比较合适。
以下方法遵循非数值模型:
使用两个字段表示货币值:一个字段将确切的货币值存储为非数字
string
,另一个字段存储该值基于二进制的浮点(double
BSON 类型)近似值。
数值模型
使用十进制 BSON 类型
decimal128
BSON 类型使用基于十进制的 IEEE 754 decimal128
浮点编号格式。与基于二进制的浮点格式(如 double
BSON 类型)不同,decimal128
不会对十进制值取近似值,并且能够提供处理货币数据所需的精确精度。
在 mongosh
中,使用 Decimal128()
构造函数对 decimal
值进行赋值和查询。以下示例将包含汽油价格的文档添加到 gasprices
集合中:
db.gasprices.insertOne( { "date" : ISODate(), "price" : Decimal128("2.099"), "station" : "Quikstop", "grade" : "regular" } )
以下查询与上述文档匹配:
db.gasprices.find( { price: Decimal128("2.099") } )
有关 decimal
类型的更多信息,请参阅 Decimal128。
将值转换为十进制
通过执行一次性转换或修改应用程序逻辑以在访问记录时执行转换,可以将集合的值转换为 decimal
类型。
提示
从版本 4.0 开始,您可以使用 $convert
及其辅助 $toDecimal
操作符将值转换为 Decimal128()
,这是下述过程的替代方法。
一次性集合转换
可以通过迭代集合中的所有文档、将货币值转换为 decimal
类型并将文档重新写入集合来转换集合。
注意
强烈建议将 decimal
值作为新字段添加到文档中,并随后在验证新字段的值后删除旧字段。
警告
请务必在隔离的测试环境中测试 decimal
转换。创建或修改数据文件之后,它们将不再与以前的版本兼容,而且不支持对包含小数的数据文件进行降级。
比例因子转换:
考虑以下集合,该集合使用比例因子方法并将货币值保存为表示美分数的 64 位整数:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") }, { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") }, { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") }, { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") }, { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }
使用 $multiply
操作符将 long
值乘以 price
和 NumberDecimal("0.01")
,可以将该值转换为适当格式的 decimal
值。以下聚合管道将转换后的值分配给 $addFields
阶段中的新 priceDec
字段:
db.clothes.aggregate( [ { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } }, { $addFields: { priceDec: { $multiply: [ "$price", NumberDecimal( "0.01" ) ] } } } ] ).forEach( ( function( doc ) { db.clothes.replaceOne( doc ); } ) )
可以使用 db.clothes.find()
查询验证聚合管道的结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }
如果您不想添加具有 decimal
值的新字段,可以覆盖原始字段。以下 updateMany()
方法首先检查 price
是否存在以及它是否为 long
,然后将 long
值转换为 decimal
并存储在 price
字段中:
db.clothes.updateMany( { price: { $type: "long" } }, { $mul: { price: NumberDecimal( "0.01" ) } } )
可以使用 db.clothes.find()
查询验证结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }
Non-Numeric Transformation:
考虑以下集合,该集合使用非数值模型并将货币值保存为具有该值的精确表示形式的 string
:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }
以下函数首先检查 price
是否存在以及它是否为 string
,然后将 string
值转换为 decimal
值并存储在 priceDec
字段中:
db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) { doc.priceDec = NumberDecimal( doc.price ); db.clothes.replaceOne( doc ); } );
该函数不会向命令行输出任何内容。可以使用 db.clothes.find()
查询验证结果:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }
应用程序逻辑转换
可以从应用程序逻辑中执行到 decimal
类型的转换。在这种情况下,应用程序被修改为在访问记录时执行转换。
典型的应用程序逻辑如下:
测试新字段是否存在以及其类型是否为
decimal
如果新的
decimal
字段不存在:通过正确转换旧字段值进行创建
移除旧字段
保留转换后的记录
使用比例因子
要使用比例因子方法对货币数据进行建模,请执行以下操作:
确定货币价值所需的最高精度。例如,您的应用程序可能要求
USD
货币值的精度低至十分之一美分。通过将货币值乘以 10 的倍数,将货币值转换为整数,从而确保所需的最高精度成为整数的最低有效数位。例如,如果所需的最高精度是十分之一美分,请将货币值乘以 1000。
存储转换后的货币值。
例如,以下将 9.99 USD
扩大 1000 倍,以保持高达十分之一美分的精度。
{ price: 9990, currency: "USD" }
该模型假定对于给定的货币值:
货币的比例因子是一致的;即给定货币的比例因子相同。
比例因子是一个常数,也是货币的已知属性;即应用程序可以根据货币确定比例因子。
使用此模型时,应用程序在执行适当的值缩放时必须保持一致。
有关此模型的使用案例,请参阅数值模型。
Non-Numeric Model
若要使用非数值模型对货币数据进行建模,请将值存储在两个字段中:
在一个字段中,将确切的货币值编码为非数值数据类型;例如,
BinData
或string
。在第二个字段中,存储精确值的双精度浮点近似值。
以下示例使用非数值模型存储 9.99 USD
作为价格,存储 0.25 USD
作为服务费:
{ price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" }, fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" } }
应用程序可以使用数值近似值对字段执行范围和排序查询,这样更加谨慎。然而,使用近似值字段进行查询和排序操作需要应用程序执行客户端后处理来解码精确值的非数值表示,然后根据精确的货币值过滤掉返回的文档。
有关此模型的使用案例,请参见非数值模型。