建模货币数据
概述
处理货币数据的程序通常需要能够捕获货币的分数单位,并在执行算术运算时精确地模拟十进制舍入。许多现代系统(例如,float,double)使用的基于二进制的浮点运算无法表示精确的十进制分数,需要一定程度的近似,这使得它不适合货币算术。在建模货币数据时,这种约束是一个重要的考虑因素。
在MongoDB中使用数值和非数值模型建模货币数据有几种方法。
数值模型
如果需要在数据库中查询精确的、数学上有效的匹配项或需要在服务器端执行算术运算,则数值模型可能是合适的,例如$inc
, $mul
, 以及 聚合管道算术。
以下方法遵循数值模型
使用十进制BSON类型,这是一种基于十进制的浮点格式,能够提供精确的精度。
使用缩放因子将货币值乘以10的幂缩放因子,转换为64位整数(
long
BSON类型)。
非数值模型
如果不需要在货币数据上执行服务器端算术运算或如果服务器端近似足够,则使用非数值模型建模货币数据可能是合适的。
以下方法遵循非数值模型
使用两个字段表示货币值:一个字段存储精确的货币值作为一个非数字
字符串
,另一个字段存储基于二进制的浮点数(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") }
可以将 long
类型的值通过使用 $multiply
操作符将 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") }
非数值转换
考虑以下集合,它使用了 非数值 模型,并将货币值保存为表示该值的精确字符串 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" }
该模型假定对于给定的货币值
缩放因子对于货币是一致的;即对于给定的货币具有相同的缩放因子。
缩放因子是货币的常数且已知属性;即应用程序可以从货币中确定缩放因子。
使用此模型时,应用程序必须在执行适当的缩放时保持一致。
有关此模型的用例,请参阅数字模型。
非数字模型
要使用非数字模型建模货币数据,将值存储在两个字段中
在一个字段中,将精确的货币值编码为非数值数据类型;例如,
BinData
或一个字符串
。在第二个字段中,存储精确值的双精度浮点近似值。
以下示例使用非数值模型存储价格 9.99 美元
和费用 0.25 美元
{ price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" }, fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" } }
通过一些谨慎的处理,应用程序可以在具有数值近似的字段上执行范围和排序查询。然而,对于查询和排序操作使用近似字段,需要应用程序执行客户端后处理,以解码精确值的非数值表示,然后根据精确货币值过滤返回的文档。
有关此模型的使用案例,请参阅 非数值模型。