iBatis.NET使用ODP.NET的注意事项
上一篇文章《.NET程序员看Oracle数据类型》总结了一些Oracle的常用数据类型和.NET类型的映射关系,这篇文章讨论如何实现它。我使用的数据库驱动是ODP.NET,数据访问框架是iBatis.NET。
一、 数值类型:
我把iBatis.NET自带的数值类型的TypeHandler全部重写了,因为这里面有这么几个问题:
1、 ODP.NET不支持SByte、UInt16、UInt32、UInt64这4种类型的参数值。事实上,在ODP.NET的OracleDbType枚举中,也没有这4种类型的枚举;而如果使用System.Data.DbType枚举中的这4种类型的枚举,或者不设置参数的OracleDbType属性和DbType属性而直接给Value属性赋值,也会出现异常。
2、 对于输出参数,如果给参数的OracleDbType属性进行了赋值,则参数的值将是ODP.NET中定义的OracleDecimal。ODP.NET的Developer’s Guide中是这么说的:
ODP.NET allows applications to obtain an output parameter as either a .NET Framework type or an ODP.NET type. The application can specify which type to return for an output parameter by setting the DbType property of the output parameter (.NET type) or the OracleDbType property (ODP.NET type) of the OracleParameter object. For example, if the output parameter is set as a DbType.String type by setting the DbType property, the output data is returned as a .NET String type. On the other hand, if the parameter is set as an OracleDbType.Char type by setting the OracleDbType property, the output data is returned as an OracleString type. If both DbType and OracleDbType properties are set before the command execution, the last setting takes affect.
问题在于iBatis.NET的数值类型的TypeHandler的实现代码中,直接使用了Convert类来进行转换,而OracleDecimal类型并不支持这个转换。
3、 对于NUMBER类型的字段,即使你确实是为存储Single/Double类型设置了精度和小数位数,但是读取时Oracle返回的类型可能是Decimal。ODP.NET的Developer’s Guide中是这么说的:
Certain methods and properties of the OracleDataReader object require ODP.NET to map a NUMBER column to a .NET type based on the precision and scale of the column. These members are:
■ Item property
■ GetFieldType method
■ GetValue method
■ GetValues method
ODP.NET determines the appropriate .NET type by considering the following .NET types in order, and selecting the first .NET type from the list that can represent the entire range of values of the column:
■ System.Byte
■ System.Int16
■ System.Int32
■ System.Int64
■ System.Single
■ System.Double
■ System.Decimal
If no .NET type exists that can represent the entire range of values of the column, then an attempt is made to represent the column values as a System.Decimal type. If the value in the column cannot be represented as System.Decimal, then an exception is raised.
For example, consider two columns defined as NUMBER(4,0) and NUMBER(10,2). The first .NET types from the previous list that can represent the entire range of values of the columns are System.Int16 and System.Double, respectively. However, consider a column defined as NUMBER(20,10). In this case, there is no .NET type that can represent the entire range of values on the column, so an attempt is made to return values in the column as a System.Decimal type. If a value in the column cannot be represented as a System.Decimal type, then an exception is raised.
但是Oracle在判断使用哪个.NET数值类型时,却不是完全精确的根据.NET数值类型的范围来确定的,因此你认为是应该返回Single/Double却有可能实际返回的类型是Decimal。iBatis.NET的SingleTypeHandler、DoubleTypeHandler、NullableSingleTypeHandler、NullableDoubleTypeHandler的实现代码中使用了强制类型转换,则就有可能发生异常。
当然,对于BINARY_FLOAT/BINARY_DOUBLE字段,其返回的类型一定是Single/Double,这可以放心的使用DataReader的GetFloat/GetDouble方法。但是需要在映射中明确指定dbType为BinaryFloat/BinaryDouble。
二、 字符类型:
iBatis.NET中实现的字符类型的TypeHandler,能很好的工作于ODP.NET驱动。
需要说明的只有一点,在使用StringTypeHandler读写CLOB/NCLOB字段时,最好在映射中明确指定dbType为Clob/NClob。
三、 日期类型:
iBatis.NET中实现的日期类型的TypeHandler,也存在的一定的问题:
1、 iBatis.NET中的TimeSpanTypeHandler和NullableTimeSpanTypeHandler,是将TimeSpan值的Ticks属性作为Int64类型的值存储到数据库中。而实际上ODP.NET直接支持存储TimeSpan值,对应的Oracle类型是INTERVAL DAY TO SECOND。当然,iBatis.NET需要支持多种数据库,为了通用性,这样处理是合理的。但是我既然确定只使用ODP.NET,就应该根据其实际情况来作出修改。
2、 从.NET 2.0 SP1开始,引入了一个新的DateTimeOffset结构,该结构包括一个 DateTime 值以及一个 Offset 属性,后者用于确定当前 DateTimeOffset 实例的日期和时间与协调世界时 (UTC) 之间的差值。而ODP.NET中也正好有TIMESTAMP WITH TIME ZONE类型,支持在存储时间的同时存储时区。所以为它们两者建立一个专门的TypeHanlder是合理的。
3、 另外需要说明的是,Oralce中的DATE类型不能存储小数秒。如果需要存储DateTime结构中的小数秒,可以使用TIMESTAMP类型,或者TIMESTAMP WITH LOCAL TIME ZONE类型(存储本地时间戳时)。
四、 二进制类型:
iBatis.NET中实现的二进制类型的TypeHandler,能很好的工作于ODP.NET驱动。
和字符类型类似,在使用ByteArrayTypeHandler读写BLOB字段时,最好在映射中明确指定dbType为Blob。
五、 布尔类型:
因为Oracle中不支持布尔类型,所以iBatis.NET中预定义的BooleanTypeHandler也就完全没有用了。
在上一篇文章中,我提到存储”Y”/”N”,”T”/”F”,1/0三种方式。我个人比较喜欢的是存储1/0,原因是如果需要,可以转成枚举。所以我自己写了一个OneZeroBooleanTypeHandler来取代预定义的BooleanTypeHandler作为Boolean类型的默认TypeHandler,另外写了两个可选的TrueFalseBooleanTypeHandlerCallback和YesNoBooleanTypeHandlerCallback作为备选。
六、 其他类型:
1. 枚举类型:
.NET枚举的基础类型(UnderlyingType)可以是SByte、UInt16、UInt32、UInt64,基于数值类型中同样的原因,我把预定义的EnumTypeHandler重写了。
2. 可序列化的类型:
序列化只能定义为TypeHandlerCallBack,因为没有一个固定的类型。这里的逻辑,只是在写入时序列化,读取时反序列而已。
3. Guid:
Oralce不支持Guid类型,因此预定义的GuidTypeHandler也没法使用。我重写了该TypeHanlder,把Guid对象转换为字符串存储到数据库中。
4. XmlDocument:
XmlDocument也是常用的一种数据类型,Oracle也支持Xml文档的存储,但是和我们这里说的不是一回事。这里的做法是把XmlDocument转换为字符串存入CLOB/NCLOB,或转换为字节数组存入BLOB。
转换的方法也很简单,使用StringReader/StringWriter可以与字符串转换,使用MemoryStream可以和字节数组转换。
照例用表格总结一下:
.NET类型 | Oracle类型 | OracleDbType (必须在映射中指定用粗体表示) | TypeHandler (自定义用粗体表示) |
Byte | NUMBER(3) | Byte | ByteTypeHandler NullableByteTypeHandler |
SByte | NUMBER(3) | Byte或Int16(根据数值范围) | SByteTypeHandler NullableSByteTypeHandler |
Int16 | NUMBER(5) | Int16 | Int16TypeHandler NullableInt16TypeHandler |
UInt16 | NUMBER(5) | Int16或Int32(根据数值范围) | UInt16TypeHandler NullableUInt16TypeHandler |
Int32 | NUMBER(10) | Int32 | Int32TypeHandler NullableInt32TypeHandler |
UInt32 | NUMBER(10) | Int32或Int64(根据数值范围) | UInt32TypeHandler NullableUInt32TypeHandler |
Int64 | NUMBER(20) | Int64 | Int64TypeHandler NullableInt64TypeHandler |
UInt64 | NUMBER(20) | Int64或Decimal(根据数值范围) | UInt64TypeHandler NullableUInt64TypeHandler |
Single | FLOAT(24) | Single | SingleTypeHandler NullableSingleTypeHandler |
Single | BINARY_SINGLE | BinaryFloat | |
Double | DOUBLE PRECISION | Double | DoubleTypeHandler NullableDoubleTypeHandler |
Double | BINARY_DOUBLE | BinaryDouble | |
Decimal | NUMBER | Decimal | DecimalTypeHandler NullableDecimalTypeHandler |
Char | VARCHAR2(1 Char) NVARCHAR2(1) | Char NChar | CharTypeHandler NullableCharTypeHandler |
String | VARCHAR2(n char) NVARCHAR2(n) CLOB NCLOB | Varchar2 NVarchar2 Clob NClob | StringTypeHandler |
DateTime | DATE TIMESTAMP TIMESTAMP WITH LOCAL TIME ZONE | Date TimeStamp TimeStampLTZ | DateTimeTypeHandler NullableDateTimeTypeHandler |
DateTimeOffset | TIMESTAMP WITH TIME ZONE | TimeStampTZ | DateTimeOffsetTypeHandler NullableDateTimeOffsetTypeHandler |
TimeSpan | INTERVAL DAY TO SECOND | IntervalDS | TimeSpanTypeHandler NullableTimeSpanTypeHandler |
Byte[] | RAW BLOB | Raw Blob | ByteArrayTypeHandler |
Boolean | NUMBER(1)(1/0) 或INTEGER(便于扩展) | Int32 | OneZeroBooleanTypeHandler NullableOneZeroBooleanTypeHandler |
Boolean | VARCHAR2(1)(’T’/’F’) | Varchar2 | TrueFalseBooleanTypeHandlerCallback NullableTrueFalseBooleanTypeHandlerCallback |
Boolean | VARCHAR2(1)(’Y’/’N’) | Varchar2 | YesNoBooleanTypeHandlerCallback NullableYesNoBooleanTypeHandlerCallback |
枚举类型 | NUMBER(n)(存储基础类型的值) 或INTEGER(便于扩展) | Byte/Int16/Int32/Int64 | EnumTypeHandler |
枚举类型 | VARCHAR2(n char)或NVARCHAR2(n)(存储常量名称) | Varchar2/Nvarchar2 | |
可序列化类型 | BLOB | Blob | SerializableTypeHandlerCallBack |
Guid | Varchar2(38)(ToString()方法) | Varchar2 | GuidTypeHandler NullableGuidTypeHandler |
XmlDocument | NCLOB/CLOB(Save到TextWriter) | NClob/Clob | XmlDocumentTypeHandler |
XmlDocument | BLOB(Save到Strema) | Blob |
参考资料:
1、 iBatis.NET源代码。我下载的版本是513437。
2、 《Oracle11g Data Provider for .NET (11.1.0.6.20) Developer's Guide .pdf》。
转发qmxle的文章。