Oracle 原生驱动带来的精度问题的分析与解决
问题
Oracle 官方提供了 dotnet core 驱动,但我们在使用中遇到了精度问题。
复现
以下代码运行数学运算 1/3,无论是 OracleCommand.ExecuteScalar()
还是 OracleDataReader.GetDecimal(0)
均会抛出异常 InvalidCastException: Specified cast is not valid.
var connectionString = "Data Source=localhost/XE;User ID=system;Password=oracle";
using (var connection = new OracleConnection(connectionString)) {
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "select 1/3 from dual";
//InvalidCastException: Specified cast is not valid.
//var scalar = command.ExecuteScalar();
var reader = command.ExecuteReader();
if (reader.HasRows) {
while (reader.Read()) {
//InvalidCastException: Specified cast is not valid.
var value = reader.GetDecimal(0);
}
}
}
排查
精度溢出的本质是数据类型不能完全匹配,以此为出发点查阅文档,得知 Oracle 返回的数据类型与 C# 版本存在不兼容问题,参考如下:
- Which .NET data type is best for mapping the NUMBER Oracle data type in NHibernate?
- Oracle Data Type Mappings
我们了解到该值被映射到了OracleDecimal
类型,应使用OracleDataReader.GetOracleDecimal()
读取。
var connectionString = "Data Source=localhost/XE;User ID=system;Password=oracle";
using (var connection = new OracleConnection(connectionString)) {
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "select 1/3 from dual";
var reader = command.ExecuteReader();
if (reader.HasRows) {
while (reader.Read()) {
var original = reader.GetOracleDecimal(0);
original.Dump("original"); //available in LINQPad
Console.WriteLine(String.Join(",", original.BinData));
}
}
}
对于一个从 Oracle 驱动获取的值为 1/3 的 OracleDecimal
类型变量 original
(Decimal)original
抛出异常OverflowException: Arithmetic operation resulted in an overflow.
Convert.ChangeType(original, TypeCode.Decimal)
抛出异常InvalidCastException4: Object must implement IConvertible
Convert.ChangeType(original.Value, TypeCode.Decimal)
抛出异常,同1,因为对 Value 的访问已经失败BitConverter.ToDouble(original.BinData, 0)
不可用,值 2.90435521010196E-144- 使用
MemoryStream
+BinaryReader.ReadDecimal()
处理字节数组original.BinData
抛出异常IOException: Decimal byte array constructor requires an array of length four containing valid decimal bytes.
分析
由第5条得知,OracleDecimal
的字节序列并不是 C# 意义上的 Decimal 字节序列,我们仍然需要借助其本身实现字节序列截断,实现如下:
OracleDecimal ToNativeDecimal(OracleDecimal value) {
var bytes = new Byte[22]; //必须使用长度为22字节,否则无法构造出 OracleDecimal
bytes[0] = 15; //告诉驱动字节长度为 16 = 15+1 位,即 .net 世界里的 decimal 长度
Array.Copy(value.BinData, 1, bytes, 1, 15); //拷贝后续15字节
return new OracleDecimal(bitData); //得得到一个 .net 世界能处理的 OracleDecimal
}
后记
数据类型映射出错导致基于 DataReader
的数据读取实现不再牢靠,基于 DbConnection
实现的数据读取类库如 Dapper 需要进一步扩展点以进行支持。
leoninew 原创,转载请保留出处 www.cnblogs.com/leoninew