1.问题描述 最近有一个需求,更新Mongo数据库中 原料 集合的某字段价格,更新后,程序报错了,说长度过长了,需要Truncation。 主要错误信息如下: 复制代码 FormatException: An error occurred while deserializing the XXXXXXXPrice property of class XXXXXXXXXXXXXXXXXXXX: Truncation resulted in data loss. 复制代码 调试发现,价格这个数据来自于SQL Server数据库,是decimal(18,4),数据落到Mongodb中也是Decimal类型。DBA通过Mongodb客户端工具更新后,更新的文档中的价格字段由Decimal类型变成了Double类型。 此时问题就出现了: (1):Double类型为15位,原来小数点后面是四位小数,现在不一定了。 (2):精确度变化,导致部分数据失真。 问题出现,我们有必要认认真真学习总结下MongoDB中的数字类型以及其余mongo shell等常见客户端工具。 在MongoDB中,关于数值的类型有: Type Alias Notes Double “double” 32-bit integer “int” 64-bit integer “long” Decimal128 “decimal” New in version 3.4 2. 数字默认为double 类型 mongo shell 客户端默认将数字看成浮点数。 例如, db.testnumber.find({t1:12345}) 查看新插入的数据, 可以看到,数字变成了Double 类型。 上面的数据插入是在mongo shell 中 验证的,其实在 nosqlbooster 工具 中,默认也是将数字当成double类型。 3 NumberLong 类型 如果想保留为int类型(64-bit integer),需要显式地通过封装函数NumberLong(),其接受的参数应为string类型。 例如,插入一笔数据 db.testnumber.insertOne( { _id: 10, calc: NumberLong("2090845886852") } ) 查看插入的数据 mongo shell 客户端查询,显式如下: 我们再来验证下通过mongo shell 工具如何对这一类型进行更新的: 复制代码 db.collection.updateOne( { _id: 10 }, { $set: { calc: NumberLong("25555550") } } ) 复制代码 显式指定 封装函数NumberLong()。 查看更新后的数据, 我们再来验证下 long 类型上的 $inc 操作($inc操作符将一个字段的值增加或者减少指定的数值) 复制代码 db.testnumber.updateOne( { _id: 10 }, ... { $inc: { calc: NumberLong(5) } } ) 复制代码 更新后,查询 上面的例子中,显式地指定了Int64 类型(通过NumberLong()函数),执行前后都是Int64。如果不指定呢?不指定就是默认的Double类型。 继续测试,在原来的基础上再加5. db.testnumber.updateOne( { _id: 10 }, ... { $inc: { calc: 5 } } ) 查看显示, 数值的类型由Int64 变成了 Double 类型。 4.32-bit integer (int) 类型 和64-bit integer(long)差不多,不同的是,其转换函数由NumberLong()变成了 NumberInt() ,其接受的参数,也当成string类型来处理。 例如: db.testnumber.insert({ts:NumberInt("246")}) 查看插入的数据: 数据类型为Int32. 5.NumberDecimal Decimal 这个数据类型是在Mongo 3.4 才开始引入的。新增Decimal数值类型主要是为了记录、处理货币数据 ,例如 财经数据、税率数据等。有时候,一些科学计算也采用Decimal类型。 因为mongo shell默认将数字当成double类型,所以也是需要显式的转换函数NumberDecimal(),其接受参数是string值。 例如: db.testnumber.insert({ts:NumberDecimal("1000.55")}) 查询显示: 我们前面,强调说,参数接受类型是string,如何是数字(默认是double类型)也可以,但是有精度丢失的风险,会把数字变成15位(小数点不计算在内)。 例如 db.testnumber.insert({ts:NumberDecimal(1000.88)}) 查看 { "_id" : ObjectId("5d5a38fa3e8964310aa46f83"), "ts" : NumberDecimal("1000.88000000000") } 再插入一笔 db.testnumber.insert({ts:NumberDecimal(1000000000.88)}) 查询这一笔数据 { "_id" : ObjectId("5d5a39103e8964310aa46f84"), "ts" : NumberDecimal("1000000000.88000") } 再插入一笔 db.testnumber.insert({ts:NumberDecimal(10000000000000.88)}) 查询变成了 { "_id" : ObjectId("5d5a3e343e8964310aa46f86"), "ts" : NumberDecimal("10000000000000.9") } 再如 需要注意的是:如果将数字类型数据作为参数传递给NumberDecimal(),只能出现在mongo shell工具中,在其他工具中可能报错。 例如在工具 nosqlbooster 中就报错。 复制代码 { "message" : "NumberDecimal param must be string.", "stack" : "script:1:29" } 复制代码 测试案例如下: 6.mongo shell 操作Decima类型 如果在mongo shell 操作Decimal,需特别小心,其数据类型和精度有可能变化。 Case 1 Decimal 类型 + Decimal 类型 Case 2 Decimal 类型 + long 类型 Case 3 Decimal 类型+ Int 类型 Case 4 Decimal 类型 + 数值 类型,即加数是默认的Double类型 Case 5 如果将两个Decimal字段相减,会是什么样子呢?我们先在mongo shell 段进行测试。 测试数据: 复制代码 { "_id" : ObjectId("5d5a50ebbd9dcf1c9b374e11"), "ts1" : NumberDecimal("32222.21111"), "ts2" : NumberDecimal("11222.21111"), "tst" : NumberDecimal("2211.11111") } { "_id" : ObjectId("5d5a50f5bd9dcf1c9b374e12"), "ts1" : NumberDecimal("22222.21111"), "ts2" : NumberDecimal("22222.21111"), "tst" : NumberDecimal("11111.11111") } 复制代码 相减操作,将tst字段设置为ts1 和 ts2的差值。 db.testnumber.find({}).forEach(function(item){ item.tst = item.ts1 - item.ts2 ;db.testnumber.save(item) }) 查询相减后的结果: 复制代码 { "_id" : ObjectId("5d5a50ebbd9dcf1c9b374e11"), "ts1" : NumberDecimal("32222.21111"), "ts2" : NumberDecimal("11222.21111"), "tst" : NaN } { "_id" : ObjectId("5d5a50f5bd9dcf1c9b374e12"), "ts1" : NumberDecimal("22222.21111"), "ts2" : NumberDecimal("22222.21111"), "tst" : NaN } 复制代码 此时出现了NAN类型。 NaN (not a number)属性代表一个“不是数字”的值。这个特殊的值是因为运算不能执行而导致的,不能执行的原因要么是因为其中的运算对象之一非数字(例如, "abc" / 4),要么是因为运算的结果非数字(例如,除数为零)。 虽然 NaN 意味着“不是数字”,但是它的类型是 Number Case 6 相加(+)操作,在mongo shell 中验证: db.testnumber.find({}).forEach(function(item){ item.tst = item.ts1 + item.ts2 ;db.testnumber.save(item) }) 此时类似string拼凑。 Case 7 相减操作如果发生在其他客户端工具,例如 nosqlbooster 工具,效果怎么样呢? 执行相减命令 db.testnumber.find({}).forEach(function(item){ item.tst = item.ts1 - item.ts2 ;db.testnumber.save(item) }) 结果截图 可知:在客户端工具 nosqlbooster 中,两个Decimal类型数据的差值是Double类型。 Case 8 在工具nosqlbooster 上执行相加的命令 db.testnumber.find({}).forEach(function(item){ item.tst = item.ts1 + item.ts2 ;db.testnumber.save(item) }) 查询结果 在客户端工具 nosqlbooster 中,两个Decimal类型数据的 和 也是Double类型。 Case 7、Case 8表明 在 客户端工具 nosqlbooster 中 ,加减两个decimal类型数据,其结果变成了Double类型。这不是我们想要的结果,极端情况,数字精确度还会变化。 Case 9 最后,我们看一个数据失真的Case 准备测试数据 复制代码 db.testnumber.insert({ ts1 : NumberDecimal("1747.872"),ts2 : NumberDecimal("51.408"),tst : NumberDecimal("123"))}) 复制代码 执行更新(在nosqlbooster 执行的) 复制代码 db.testnumber.find({}).forEach(function(item){ item.tst = item.ts1 - item.ts2 ;db.testnumber.save(item) }) 复制代码 更新后的数据 复制代码 { "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : 1696.4640000000002 } 复制代码 tst 字段,变成了Double类型,且计算后的结果是不准确的。 7.保持Decimal 字段类型及精度的尝试 那么有没有其他写法,可以保证更新前后数据类型不变并且不会失真呢? 7.1先寻找保持数据类型不变的方法 如果是 nosqlbooster 工具,将要更新的字段保留为NumberDecimal,其操作命令如下: 复制代码 db.testnumber.find({}).forEach(function(item){ db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})}) 复制代码 查看更新的结果 但是这个命令是不可以在 mongo shell 段执行的,测试如下: 在mongo shell执行如下命令: 复制代码 db.testnumber.find({}).forEach(function(item){ db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})}) 复制代码 更新结果如下: 上面的数据类型虽然是Decimal,但是数字是NAN。所以不能更新执行。 7.2 数据不失真问题 还是使用上面第6 部分的Case 数据。 测试前的数据 复制代码 db.testnumber.insert({ ts1 : NumberDecimal("1747.872"),ts2 : NumberDecimal("51.408"),tst : NumberDecimal("123"))}) 复制代码 执行更新(在nosqlbooster 执行的) 复制代码 db.testnumber.find({}).forEach(function(item){ db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String(item.ts1 - item.ts2))}})}) 复制代码 更新后的数据 复制代码 { "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : NumberDecimal("1696.4640000000002") } 复制代码 tst 字段,已经变成了Decimal类型,但计算后的结果是不准确的。 我们在开篇讲过,原来的数据都是保存了Decimal(18,4)的格式,所以,如果在mongo 命令上添加四舍五入的函数 toFixed(n) , n为要保留的小数位数。 复制代码 db.testnumber.find({}).forEach(function(item){ db.testnumber.update({"_id":item._id},{$set:{"tst":NumberDecimal(String((item.ts1 - item.ts2).toFixed(4)))}})}) 复制代码 查询结果 复制代码 { "_id" : ObjectId("5d5b922744b6e6393c6c7693"), "ts1" : NumberDecimal("1747.872"), "ts2" : NumberDecimal("51.408"), "tst" : NumberDecimal("1696.4640") } 复制代码 这个结果才是我们真正想要的结果。 8.不同数字类型下的比较 查询 测试案例所需数据 复制代码 db.testnumno.insert({ "_id" : 1, "val" : NumberDecimal( "9.99" ), "description" : "Decimal" }) db.testnumno.insert({ "_id" : 2, "val" : 9.99, "description" : "Double" }) db.testnumno.insert({ "_id" : 3, "val" : 10, "description" : "Double" }) db.testnumno.insert({ "_id" : 4, "val" : NumberLong(10), "description" : "Long" }) db.testnumno.insert({ "_id" : 5, "val" : NumberDecimal( "10.0" ), "description" : "Decimal" }) 复制代码 Case 1 执行查询 db.testnumno.find({ "val": 9.99 }) 返回结果 { "_id" : 2, "val" : 9.99, "description" : "Double" } 直接输入数字,默认是Double类型,在算法表示上 double 类型的9.99 和 Decimal 类型的9.99 是不相等的。查询结果只有一条数据。 Case 2 执行查询 db.testnumno.find({ "val": NumberDecimal( "9.99" ) }) 返回结果 { "_id" : 1, "val" : NumberDecimal("9.99"), "description" : "Decimal" } 返回一条结果的原因和Case 1 相同。 Case 3 执行查询 db.testnumno.find({ val: 10 }) 返回结果 复制代码 { "_id" : 3, "val" : 10, "description" : "Double" } { "_id" : 4, "val" : NumberLong(10), "description" : "Long" } { "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" } 复制代码 Case 4 执行查询 db.testnumno.find({ val: NumberDecimal( "10" ) }) 返回结果 复制代码 { "_id" : 3, "val" : 10, "description" : "Double" } { "_id" : 4, "val" : NumberLong(10), "description" : "Long" } { "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" } 复制代码 Case 5 执行查询 db.testnumno.find({ val: NumberDecimal( "10.0" ) }) 返回结果 复制代码 { "_id" : 3, "val" : 10, "description" : "Double" } { "_id" : 4, "val" : NumberLong(10), "description" : "Long" } { "_id" : 5, "val" : NumberDecimal("10.0"), "description" : "Decimal" } 复制代码 Case 3、Case 4 、Case 5 表明,在表达整数时,doubel 、Decimal 、Long 三者在算法表达上相等。 以上 5 个Case 在Mongo shell、nosqlbooster 演示结果一样。 参考文献: https://docs.microsoft.com/en-us/dotnet/api/system.double?redirectedfrom=MSDN&view=netframework-4.8 https://docs.mongodb.com/manual/core/shell-types/ https://docs.mongodb.com/manual/reference/operator/query/type/index.html https://www.jianshu.com/p/6b51adc05203 https://stackoverflow.com/questions/5314238/how-do-i-set-the-serialization-options-for-the-geo-values-using-the-official-10g https://www.213.name/archives/1147 本文版权归作者所有,未经作者同意不得转载,谢谢配合!!!https://www.cnblogs.com/xuliuzai/p/11365739.html