文档菜单
文档首页
/
MongoDB 手册
/ / /

建模货币数据

本页内容

  • 概述
  • 数值模型
  • 非数值模型

处理货币数据的程序通常需要能够捕获货币的分数单位,并在执行算术运算时精确地模拟十进制舍入。许多现代系统(例如,float,double)使用的基于二进制的浮点运算无法表示精确的十进制分数,需要一定程度的近似,这使得它不适合货币算术。在建模货币数据时,这种约束是一个重要的考虑因素。

在MongoDB中使用数值和非数值模型建模货币数据有几种方法。

如果需要在数据库中查询精确的、数学上有效的匹配项或需要在服务器端执行算术运算,则数值模型可能是合适的,例如$inc, $mul, 以及 聚合管道算术。

以下方法遵循数值模型

  • 使用十进制BSON类型,这是一种基于十进制的浮点格式,能够提供精确的精度。

  • 使用缩放因子将货币值乘以10的幂缩放因子,转换为64位整数(long BSON类型)。

如果不需要在货币数据上执行服务器端算术运算或如果服务器端近似足够,则使用非数值模型建模货币数据可能是合适的。

以下方法遵循非数值模型

  • 使用两个字段表示货币值:一个字段存储精确的货币值作为一个非数字 字符串,另一个字段存储基于二进制的浮点数(double BSON类型)对值的近似。

注意

本页所述的算术运算是指由 mongodmongos 在服务器端执行的算术运算,而不是客户端算术运算。

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 操作符将 priceNumberDecimal("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 字段不存在

    • 通过适当转换旧字段值来创建它

    • 删除旧字段

    • 持久化转换后的记录

注意

在建模货币数据时,使用十进制类型比使用缩放因子方法更受欢迎。

要使用缩放因子方法来建模货币数据

  1. 确定货币值所需的最大精度。例如,您的应用程序可能需要将USD货币值的精度精确到一分之一的美分。

  2. 通过将值乘以一个10的幂,将货币值转换为整数,确保所需的最大精度成为整数的最低有效位。例如,如果所需的最大精度是一分之一的美分,则将货币值乘以1000。

  3. 存储转换后的货币值。

例如,以下缩放将9.99 USD乘以1000,以保留到一分之一的美分的精度。

{ price: 9990, currency: "USD" }

该模型假定对于给定的货币值

  • 缩放因子对于货币是一致的;即对于给定的货币具有相同的缩放因子。

  • 缩放因子是货币的常数且已知属性;即应用程序可以从货币中确定缩放因子。

使用此模型时,应用程序必须在执行适当的缩放时保持一致。

有关此模型的用例,请参阅数字模型。

要使用非数字模型建模货币数据,将值存储在两个字段中

  1. 在一个字段中,将精确的货币值编码为非数值数据类型;例如,BinData或一个字符串

  2. 在第二个字段中,存储精确值的双精度浮点近似值。

以下示例使用非数值模型存储价格 9.99 美元 和费用 0.25 美元

{
price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" },
fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" }
}

通过一些谨慎的处理,应用程序可以在具有数值近似的字段上执行范围和排序查询。然而,对于查询和排序操作使用近似字段,需要应用程序执行客户端后处理,以解码精确值的非数值表示,然后根据精确货币值过滤返回的文档。

有关此模型的使用案例,请参阅 非数值模型。

返回

关键字搜索