项目环境:VS2008+Castle ActiveRecord1.0.3(基于NHibernate1.2.0)+ SQLServer2005。我们这个项目要求既可以支持SQL Server2005数据库,也可以支持Oracle10g数据库,所以现在需要把SQLServer2005中的所有表和存储过程迁移到Oracle10g里。

      这个项目的每个表的主键都是Guid类型,在Oracle里面,是应该使用char(38)还是raw(16)来保存Guid类型数据呢?事实上,无论使用char(38)还是raw(16),Nhibernate都会
抛出无法进行类型转换的异常。究其原因,要从OracleParameter的DbType和OracleType的对应关系说起。

OracleParameter的DbType和OracleType的对应关系

      执行:

      System.Data.OracleClient.OracleParameter parm = new System.Data.OracleClient.OracleParameter();

      parm.DbType = DbType.Guid; 

      会发现当执行了“parm.DbType = DbType.Guid; ”之后,“parm.OracleType”的值会自动变为“Raw”;如果执行的是“parm.DbType = DbType.AnsiStringFixedLength”,“parm.OracleType”的值就会自动变为“Char”。也就是说,OracleParameter 对象会自动维护DbType和OracleType之间的对应关系。那么SqlParameter的对应关系是怎样的呢?

SqlParameter的DbType和SqlDbType的对应关系

      执行:

      System.Data.SqlClient.SqlParameter parm = new System.Data.SqlClient.SqlParameter();

      parm.DbType = DbType.Guid;

      会发现当执行了“parm.DbType = DbType.Guid; ”之后,“parm.SqlDbType”的值会自动变为“UniqueIdentifier”;如果执行的是“parm.DbType = DbType.AnsiStringFixedLength”,“parm.SqlDbType”的值也会自动变为“Char”。

抛出异常的原因

      也就是说,同样是DbType.Guid,在SqlParameter和OracleParameter里面对应的SqlDbType/OracleType的值是不同的(OracleType里面压根没有UniqueIdentifier类型)。

      而NHibernate\Type\GuidType.cs里的Set()函数直接把parm.Value赋值为一个Guid对象:

      public override void Set(IDbCommand cmd, object value, int index)

      {

            IDataParameter parm = cmd.Parameters[index] as IDataParameter;

            parm.Value = value;

      }

      在OracleClient组件里,定义了“LONGRAW”和“RAW”类型对应的数据类型都是“byte[]”。

      当NHibernate调用OracleCommand.Execute()时,OracleCommand.Execute()又会调用OracleParameter.CoerceValue(),OracleParameter.CoerceValue会调用System.Convert.ChangeType(aGuidObject, typeof(Byte[]), null),而Guid又没有实现IConvertible接口,这时就会抛出“InvalidCastException: 对象必须实现 IConvertible。”的异常了。

解决方案1:在Oracle数据库中使用raw(16)类型保存Guid,修改NHibernate源代码

修改NHibernate\Type\GuidType.cs里的Set()和Get()函数:


public
 
override
 
void
 Set(IDbCommand cmd, 
object
 value, 
int
 index)
{
      IDataParameter parm 

=
 cmd.Parameters[index] 
as
 IDataParameter;
      

bool
 oracle 
=
 (cmd.GetType().FullName 
==
 

System.Data.OracleClient.OracleCommand

);
      parm.Value 

=
 oracle 
?
 ((Guid)value).g.ToByteArray() : value;
}

public
 
override
 
object
 Get(IDataReader rs, 
string
 name)
{
      System.Type type 

=
 value.GetType();
      

if
 (type 
==
 
typeof
(
string
)) 
return
 
new
 Guid((
string
)value);
      

if
 (type 
==
 
typeof
(Guid)) 
return
 value;
      

if
 (type 
==
 
typeof
(
byte
[])) 
return
 
new
 Guid((
byte
[])value);
      

return
 
null
;
}


      在Oracle里用raw(16)存储Guid数据有一个缺点:在.net里,Guid.ToByteArray()并不是简单地把Guid字符串里的“-”去掉,而是会把前几位进行一系列移位运算,例如:

      Guid id = new Guid(“dfd94f82-b680-44a5-be14-4b4a4350bf43”);

      byte[] b = id.ToByteArray(); 

      b的值将会是“824FD9DF80B6A544BE144B4A4350BF43”(保存到数据库里的也是这个数据),这样会对开发时的调试、排错工作造成很多困扰。再加上我们项目的.net源代码和SQLServer存储过程里面还有一些硬编码的Guid字符串常量,如果raw字段与Guid的字符串看上去不一样,会是很麻烦的事儿。

解决方案2:在Oracle里用char(38)保持Guid数据,修改NHibernate源代码

只需修改NHibernate\Type\GuidType.cs里的Set()函数,Get()函数不要修改:


public
 
override
 
void
 Set(IDbCommand cmd, 
object
 value, 
int
 index)
{
      IDataParameter parm 

=
 cmd.Parameters[index] 
as
 IDataParameter;
      

bool
 oracle 
=
 (cmd.GetType().FullName 
==
 

System.Data.OracleClient.OracleCommand

);
      parm.Value 

=
 oracle 
?
 ((Guid)value).ToString().ToUpper() : value;
      

if
 (oracle)
      {
          parm.DbType 

=
 DbType.AnsiStringFixedLength;
      }
}

public
 
override
 
object
 Get(IDataReader rs, 
string
 name)
{
    

return
 
new
 Guid(Convert.ToString(rs[name]));
}


 缺点:使用char(38)要比raw(16)多占用存储空间。更严重的是,由于Oracle的字符串比较是区分大小写的,一旦不小心把字段值与一个小写的Guid字符串比较,就会匹配不了了(更为不爽的是,Guid.ToString()出来的字符串就是小写的!)。即使这样,我还是比较喜欢这个方案。

解决方案3:在Oracle里使用char(38),不修改NHibernate源代码,使用自定义类型

      如果不想修改NHibernate源代码,可以用自定义类型。首先,定义一个自定义类型“DawnGuid”,放到名为“GuidTest.CustomType”的类库中:




DawnGuid

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate;
using System.Data;
using NHibernate.Type;
using NHibernate.SqlTypes;

namespace GuidTest.CustomType
{
    
public class DawnGuid : ValueTypeType, IDiscriminatorType
    
{
        Guid _guidValue 
= Guid.Empty;

        
public Guid GuidValue
        
{
            
get return _guidValue; }
        }


        
public DawnGuid()
            : 
base(SqlTypeFactory.Guid)
        
{
        }


        
public DawnGuid(string arg) : base(SqlTypeFactory.Guid)
        
{
            _guidValue 
= new Guid(arg);
        }


        
public override string ObjectToSQLString(object val)
        
{
            
return  + val.ToString().ToUpper() + 
        }


        
public override void Set(IDbCommand cmd, object value, int index)
        
{
            IDataParameter parm 
= cmd.Parameters[index] as IDataParameter;

            
bool oracle = (cmd.GetType().FullName == System.Data.OracleClient.OracleCommand);
            parm.Value 
= oracle ? ((DawnGuid)value).ToString().ToUpper() : value;
            
if (oracle)
            
{
                parm.DbType 
= DbType.AnsiStringFixedLength;
            }

        }


        
public override object Get(IDataReader rs, int index)
        
{
            
return new DawnGuid(Convert.ToString(rs[index]));
        }


        
public override object Get(IDataReader rs, string name)
        
{
            
return new DawnGuid(Convert.ToString(rs[name]));
        }


        
public override object FromStringValue(string xml)
        
{
            
return new DawnGuid(xml);
        }


        
public override string Name
        
{
            
get return DawnGuid; }
        }


        
public override Type ReturnedClass
        
{
            
get return typeof(DawnGuid); }
        }


        
IIdentifierType 成员#region IIdentifierType 成员

        
public object StringToObject(string xml)
        
{
            
return new DawnGuid(xml);
        }


        
#endregion


        
public override string ToString()
        
{
            
return _guidValue.ToString().ToUpper();
        }

    }

}

注意 上面的代码并没有包括模仿Guid的全部构造函数,和必要的对“==”、“Equals”、“ToString()”等函数的重载,如果你要使用这个方案,一定要实现它们。

然后,把实体和Client代码中的Guid全部换成DawnGuid:




实体

using System;
using System.Collections.Generic;
using System.Text;
using Castle.ActiveRecord;
using GuidTest.CustomType;

namespace Test.DataEntity
{
    
/**//// <summary>
    
/// Use Custom Type Demo
    
/// </summary>

    [ActiveRecord(GUIDTEST2)]
    
public class GuidTest2Entity : ActiveRecordBase<GuidTest2Entity>
    
{
        
private DawnGuid _stuId;
        
private DawnGuid _teacherId;
        
private System.String _stuName;

        [PrimaryKey(PrimaryKeyType.Assigned, ColumnType 
= GuidTest.CustomType.DawnGuid,GuidTest.CustomType)]
        
public DawnGuid StuId
        
{
            
get return this._stuId; }
            
set this._stuId = value; }
        }


        [Property(ColumnType 
= GuidTest.CustomType.DawnGuid,GuidTest.CustomType)]
        
public DawnGuid TeacherId
        
{
            
get return this._teacherId; }
            
set this._teacherId = value; }
        }


        [Property]
        
public System.String StuName
        
{
            
get return this._stuName; }
            
set this._stuName = value; }
        }

    }

}


如果想用Oralce中想用raw类型,自定义类型的写法就与方案1差不多,不再赘述。