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# 版本存在不兼容问题,参考如下:

我们了解到该值被映射到了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

  1. (Decimal)original
    抛出异常 OverflowException: Arithmetic operation resulted in an overflow.
  2. Convert.ChangeType(original, TypeCode.Decimal)
    抛出异常 InvalidCastException4: Object must implement IConvertible
  3. Convert.ChangeType(original.Value, TypeCode.Decimal)
    抛出异常,同1,因为对 Value 的访问已经失败
  4. BitConverter.ToDouble(original.BinData, 0)
    不可用,值 2.90435521010196E-144
  5. 使用 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

posted @ 2019-11-28 10:17  leoninew  阅读(488)  评论(0编辑  收藏  举报