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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构