个人orm开发记录

第一次知道orm概念是在今年,每天浏览博客园或多或少的也看到些关于orm的讨论,也不乏一些orm动手系列,不过自己只停留在知道与浏览过的概念。

    由于在公司单兵作战,从一开始接手的项目是一套C++/CLI的项目,所有数据操作都是简单的CURD,没有分层概念,甚至被不允许分模块开发,所有东西多在同一个Project中,UI后台代码中处处是SQL语句,很是被动。

    后来工作的几个月中,我不断的修改,不断的改进,甚至不断的跟上司闹矛盾,最终使项目有了一些改进,这些改进包括分层、分模块的开发,组件的使用,全部代码的重写等。

    这些改进虽然不是很多很难,但是期间由于上司的不理解,不允许,不支持,到现在还包括不能使用ORM,不接受C++/CLI以外的代码,不接受config文件等等一些问题的存在。

    我也提出过辞职,但在老总的挽留和其他一些原因下我还是在坚持,我尽自己的能力使公司的产品向好的方向发展,我想最终我的上司也会有所改变。

 

回到正题,在工作的时候我尝试开发下面这样一套简单的ORM,如果以后的有一天被允许用在公司的项目中,我想我的这次学习也没有错。

     本着最小接口的原则,开发初期首先要确定需要提供的接口,所以在我的脑子里描述出了以下2个接口:

    第一个是DAL的接口:

                       个人orm开发记录 

   在IDataProvider接口中我需要基础数据库提供基本的4Excute操作; 

   第二个是IEntity 的接口:

                       个人orm开发记录

    IEntity接口也很简单,对一个实体的操作我需要的是基本的CURD操作; 

 

    确定了这2个基础接口的想法后,我开始了尝试开发,对于实现IDataProvider接口缩小了说就是一个SqlHelper的东西,放大点就是需要兼容SQLAccessMySql等各类数据库的一个DAL,所以在命名上的我选择了Provider也就是不想它只是一个SqlHelper

   在之前我已经看过并简化过Microsoft Enterprise Library 5.0Data Block的代码,所以对于IDataProvider的实现也很轻松的就完成了,基本仿照Enterprise Library 实现了如下代码: 

   个人orm开发记录 

 

   其中 DataProvider是实现IDataProvider接口的基类,并且定义成了抽象基类,要能够实例这个DataProvider 又派生了一个继承类 GenericProvider

   其实从确定使用最小接口开发到时候,我就想提供最少的接口给外部调用,所以只留了 IDataProviderpublic的,其他的都为 internal级别,这个接口要能实例一个对象,则必须要提供一个public方法获取实例好的接口对象,所以我又提供了一个静态类DbHelper  ,最初的DbHelper类代码如下: 

 public static class DbHelper

    {
         ///   <summary>
        
///  获取IDataProvider接口实例
        
///   </summary>
        
///   <param name="provider"> 数据库类型 </param>
        
///   <param name="conn"> 连接字符串 </param>
        
///   <returns> 接口实例 </returns>
         public   static  IDataProvider CreateDataProvider(ProviderType provider, string  conn)
        {
            IDataProvider DbProvider 
=   null ;
            
string  character  =   " @ " ;
            
switch  (provider)
            { 
                
case  ProviderType.SQLSERVER:
                     DbProvider 
=   new  GenericProvider(conn, SqlClientFactory.Instance);
                     
break ;

                
                
case  ProviderType.ACCESS:
                     DbProvider 
=   new  GenericProvider(conn, OleDbFactory.Instance);
                     
break ;
                
/*
                case ProviderType.ORACLE:
                    character=":";
                    DbProvider = new GenericProvider(conn,OracleClientFactory.Instance);
                    break;
                
*/
                
default :
                    
throw   new  NotImplementedException( " 数据库类型不支持 " );
            }
            DbProvider.ParameterCharacter 
=  character;     // 此处要设置下 ParameterCharacter
             return  DbProvider;
        }
}

 

到这一步,一个基本的DAL已经完成了。

 

    对于实现IEntity,还是绕了一些弯路的,一开始我的想法是,一个泛型基类 EntityBase<TEntity> 实现IEntity接口,然后由实体继承 EntityBase,这样实体就拥有了 Save Update Delete等方法。 

    同时也查找了一些文章发现也确有很多如此实现的,但是除了 SaveUpdate放之外,对于Find方法如果被约束为实例方法则好像不太合理,如果约束为静态方法,那么实体继承后,用实体Find出实体好像也不太让我满意。例如: 

    class UserInfo:EntityBase<UserInfo>{  }

使用时:

1

IList<UserInfo> users= UserInfo.FindAll();

2

UserInfo user=new UserInfo();

    IList<UserInfo> users=user.FindAll();

这两种方法都会让我看起来感觉不满意,所以在IEntity的实现上我最后并没有选择让实体都继承一个EntityBase这种方式,

于是我重新定义了 IEntityHelper接口,想通过一个 EntityHelper实现了进行对实体的CURD操作,

    重新定义的IEntityHelper接口如下: 

     /// <summary>

     ///  实体辅助接口
    
///   </summary>
     public   interface  IEntityHelper
    {
        
///   <summary>
        
///  SQL语句调试
        
///   </summary>
         event  EventHandler < DebugSqlArgs >  DebugSql;

        
///   <summary>
        
///  新增
        
///   </summary>
        
///   <typeparam name="TEntity"></typeparam>
        
///   <param name="entity"></param>
        
///   <returns></returns>
         bool  Insert < TEntity > (TEntity entity)  where  TEntity :  new ();

        
///   <summary>
        
///  删除
        
///   </summary>
        
///   <typeparam name="TEntity"></typeparam>
        
///   <param name="entity"></param>
        
///   <returns></returns>
         bool  Delete < TEntity > (TEntity entity)  where  TEntity :  new ();

        
///   <summary>
        
///  更新
        
///   </summary>
        
///   <typeparam name="TEntity"></typeparam>
        
///   <param name="entity"></param>
        
///   <returns></returns>
         bool  Update < TEntity > (TEntity entity)  where  TEntity :  new ();

        
///   <summary>
        
///  查找
        
///   </summary>
        
///   <typeparam name="TEntity"></typeparam>
        
///   <param name="querys"></param>
        
///   <returns></returns>
        IList < TEntity >  Select < TEntity > ( params  IQuery[] querys)  where  TEntity :  new ();

        
///   <summary>
        
///  查找一项
        
///   </summary>
        
///   <typeparam name="TEntity"></typeparam>
        
///   <param name="querys"></param>
        
///   <returns></returns>
        TEntity FindOne < TEntity > ( params  IQuery[] querys)  where  TEntity :  new ();
       
    }

 

      对于ORM中比较重要的SQL语句的生成,我定义了一个IQuery接口以便在Select查询的时候传递查询条件, 并用一个单独的 SqlBulider 静态类提供的方法生成SQL语句。

确立了上面的想法之后就是实际的EntityHelper的开发了, 实际上当我们去了解orm的时候就知道,Object Relation Mapping就是建立关系数据与实体对象的映射,如何将实体对象与关系数据()进行映射,就需要对象能够描述表 ,比如表名、字段、字段的属性等等。

首先要建立对象与表对应的关系的时候,有几种建立方法,用配置文件,用字段、属性描述,这些都能达到实体对象与表建立一个对应关系,但是并不是一个好的处理方法。有很多前辈已经研究过这些方面,并提出了用Attribute进行描述的方式,这里我也很自然的选择使用这种方式加入到我的开发。

 当 然,本着尝试开发的心态,并没有想一下开发或者构思好一个完整的框架,所以能省略的东西我也省略了,包括对字段的描述只标注简单的属性: 

     /// <summary>

     ///  描述字段结构
    
///   </summary>
     internal   class  Field
    {
        
///   <summary>
        
///  字段属性
        
///   </summary>
         internal  PropertyInfo Property
        { 
            
get ;
            
set ;
        }

        
///   <summary>
        
///  字段名(对应数据库字段名)
        
///   </summary>
         internal   string  Name
        {
            
get ;
            
set ;
        }

        
///   <summary>
        
///  字段类型
        
///   </summary>
         internal  DbType Type
        {
            
get ;
            
set ;
        }

        
///   <summary>
        
///  是否为标识 
        
///   </summary>
         internal   bool  Identify
        {
            
get ;
            
set ;
        }

        
///   <summary>
        
///  是否为主键
        
///   </summary>
         internal   bool  PrimaryKey
        {
            
get ;
            
set ;
        }
    }

 

     有了把实体对象和数据表进行对应这一基础之后,我们需要考虑的就是真正的 SaveDelete这些方法执行时是如何将一个实体对象的属性解析出来(这里就相当于在准备SQL操作时,如何从实体对象中知知道与其对应的数据表操作的信息), 这里实际有一个前提, 我们的EntityHelper Save等方法都是泛型方法,所以它们自身并不知道传递进去的对象是怎样一个结构,也并不到这个结构具体的数据。

    通过学习我们知道.net中的反射机制,通过反射来动态的解析一个对象在中变比较简单,深层次的原理我们不必要知道,做为最普通的“IT码工”,站在我的层次上我只要能够使用并知道大概就可以了。

所以在前人的基础上,我的orm也自然离不开用反射获取从实体对象到SQL操作需要的表信息,但是在学习的基础上,也离不开我们自己的思考,反射相对于直接操作是比较耗费时间的,所以此时就有一个orm与直接数据操作的效率问题成为了大家关注的问题。 

很多前辈 很多orm讨论已经告诉我们需要用缓存来降低发射对性能的消耗,所以我的orm中也必须使用缓存来降低反射这种损耗。 

在学习过程中我没有直接接触那些比较大的orm框架,都只是查看一些讨论的博文,顺便下载了一些博文附带的orm实现示例,在此过程中学到了很多但也发现了一些问题:

如:有的orm根本没有注意反射对性能的影响,没有一个缓存机制,有的orm在执行 Svaedelete等方法时返回void类型 根本无法判断执行是否成功,还有的明确要求对于数据表要包含自增主键,但是它自身的Save方法执行后并没有获取自增主键值来更新实体的属性;

 

这些问题虽然致使这些组件不能很好的做实际应用,但是从中学习到的是更多的东西,通过这些也让自己思考如何更好的实现,如何避免这些错误的方式方法。 

到此为止,首先通过自己思考提出最小接口,再通过学习现有orm的讨论和实现示例,然后确定一个开发流程做到心中有数。心中有数了,其实就变的就非常简单了,按照构思好的接口,实现一个EntityHelper就可以了,对于如何实现EntityHelper 在查阅资料与学习的过程中已经有了清晰的线路,我自己简要概括出了如下路线 :

    个人orm开发记录 

     由于是尝试性的按最小开发,所以我的要求是开发一个简单基础的orm,此处的orm可能并不是真正的orm,只是一个能让实体通过Save等面向对象方法来代替数据库Excute操作的东西。 

   有了思考之后是实际的开发,实际开发其实也就是EntityHelper的开发,在SaveUpdateDelete方法实现时都比较简单,基本实现类似如下: 

    /// <summary>

         ///  删除
        
///   </summary>
        
///   <typeparam name="TEntity"> 实体类型 </typeparam>
        
///   <param name="entity"> 实体对象 </param>
        
///   <returns> 成功/失败 </returns>
         public   bool  Delete < TEntity > (TEntity entity)  where  TEntity :  new ()
        {
            Table table 
=  TableFactory.GetTable( typeof (TEntity));
            
string  sqlText  =  SqlBulider.Delete(table, entity, DbProvider.ParameterCharacter);
            DbParameter parameter 
=  DbProvider.Parameter;
            
foreach  (var field  in  table.Fields)
            {
                
if  (field.PrimaryKey)              // 删除时根据主键删除
                {
                    parameter.ParameterName 
=  DbProvider.ParameterCharacter  +  field.Name;
                    
object  value  =  field.Property.GetValue(entity,  null );
                    parameter.Value 
=  value  ==   null   ?  DBNull.Value : value;   // 取字段值
                    parameter.DbType  =  field.Type;
                    
break ;
                }
            }
            StringBuilder debugSql 
=   new  StringBuilder(sqlText);
            debugSql.Append(Environment.NewLine);
            debugSql.Append(
" < " );
            debugSql.Append(parameter.ParameterName);
            debugSql.Append(
" = " );
            
if  (parameter.Value  !=   null )
                debugSql.Append(parameter.Value.ToString());
            debugSql.Append(
" > " );
            
if  (_debugsql  !=   null )
                _debugsql(
this new  DebugSqlArgs( " Delete " typeof (TEntity).Name, debugSql.ToString()));
            
return  DbProvider.ExecuteNonQuery(sqlText, parameter)  ==   1 ;
        }

 

   实现Delete方法的流程是:

个人orm开发记录 

      Find方法实现时有些不同,主要就是Find方法实现的是数据库的Select操作,往往包含一些查询条件,如 =  between like order by 这些操作,所以此处就有点为了ormorm的意味了, 为了避免Find方法的参数包含直接的SQL语句,所以我定义了一个IQuery的接口:

     个人orm开发记录 

     这个接口比较简单,有一个方法 Context 为输出sql查询子句, 后来在分析过程中考虑到由于实体属性对应数据表字段,而且所有的Excute操作都要参数传递的,所以在后面的分析思考中又添加了一个字段名,和一个 Hashtable表存放参数值; 

    IQuery

 

为什么用Hashtable存放参数值呢,是因为出现类似  between查询时 形如: where id between @id1 and @id2  ,这个时候拥有2个参数@id1,@id2 不能全部由字段名id来表示,所以需要一个Hashtable存在该查询对象的查询参数。

 

    这样大概的一个基本的orm形态就完成了。后面的一些工作就是一些细节方面的东西了,主要的细节包括以下: 

1,  EntityHelper中的IDataProvider对象的存在方式,是静态成员还是实例成员?EntityHelper 是否可以为静态类,Save等方法是否可以为静态方法?

     在考虑这个问题的时候其实要联系到 前面提到的DbHelperDbHelperstatic IDataProvider CreateDataProvider(ProviderType provider,string conn) 方法根据传递的ProviderType conn来确定使用何种数据库对象,

同时提供它的一个重载方法static IDataProvider CreateDataProvider()通过这个重载方法大家肯定会想到一个DataProvider应该有一个通过配置文件配置所使用数据库类型的功能,我的orm也不例外, 不过只是一个简单的实现用一个可序列化的对象ProviderConfig序列为XML文件来保存的。

   那这个跟EntityHelper有什么关系呢,我们一般的情况下可能一个程序中使用一种数据库就可以了,此时我的EntityHelper种的DataProvider对象不管是静态的也好,还是实例化的也好,可能不需要太注意,应为一个程序中只可能存在一种数据库操作的DataProvider 

    现在就提出了一个问题,假如我有一个Access数据库,一个SQL数据库,我想在程序中将SQL的数据添加到Access中,那此时用一个 EntityHelper 操作时 DataProvider就只能是实例成员了,因为我可以通过static IDataProvider CreateDataProvider(ProviderType provider,string conn)方法实例2个不同数据库的DataProvider,同时实例2个不同的EntityHelper对象来操作2种数据库; 

   这样上面的这个问题就解决了。

 

2,  如果使用缓存减少反射的性能损耗,在哪里使用缓存,这也是一个问题?

    实际上通过实现Save等方法时,第一步就是反射实体,这里我们自然的就想到 能否将实体结构缓存起来不用每次都反射呢? 答案当然是肯定的,面向对象的开发里就是需要将所见所想能抽象出来用对象描述。这里我实现了另外2个类用来这部分操作:

   个人orm开发记录 

    这样在Save方法中第一句代码Table table = TableFactory.GetTable(typeof(TEntity));

   就可以想到TableFactory.GetTable方法是要从缓存判断有没有Tentity对象的结构,来看下这个方法就比较清楚了: 

    /// <summary>

     ///  用于获取和缓存Table对象,避免多次反射同一对象
    
///   </summary>
     internal   class  TableFactory
    {
        
///   <summary>
        
///  缓存Table对象
        
///   </summary>
         private   static  Dictionary < string , Table >  cache  =   new  Dictionary < string , Table > ();

        
///   <summary>
        
///  获取Table对象
        
///   </summary>
        
///   <param name="type"> 对象类型 </param>
        
///   <returns> Table对象 </returns>
         public   static  Table GetTable(Type type)
        {
            
if  (cache.ContainsKey(type.FullName))    // 使用类型FullName做Key
            {
                
return  cache[type.FullName];
            }
            
return  BulidTable(type);
        }

        
///   <summary>
        
///  创建一个新的Table对象 并将其添加到Table缓存里
        
///   </summary>
        
///   <param name="type"> 对象类型 </param>
        
///   <returns> 新的Table对象 </returns>
         private   static  Table BulidTable(Type type)
        {
            Table table 
=   new  Table(type);  // 创建新的对象
            cache.Add(type.FullName, table);
            
return  table;
        }
    }

 

     通过上面的方法也解决了缓存问题。 虽然是一个简单基本的缓存,但是这仅仅只是用在这个尝试性的orm开发中,所以这一切都只是一个学习,也足够了。

 

3,  接下来来的一个问题是参数符号,也就是“@” 和 “:”

    这个问题在一开始开发DataProvider时我并没有想到,所以这个时候就不得不去修改IDataProvider接口,为其添加下面2个属性:

             /// <summary>

        /// DbCommand参数实例

        /// </summary>

        DbParameter Parameter{get;}

 

        /// <summary>

        /// 参数符号

        /// </summary>

        string ParameterCharacter { get; set;} 

 

     至于为什么要这么添加,也是由于Save方法的实现需要和DataProvider的已经完成,所以这里也让我知道设计是多么的重要,一个好的设计可以适合很多复杂情景并且不会有问题。

对于这一处修改而牵扯的其他修改就更不必说了。 

 

4,  Save方法中还有一个很重要的方面就是前面提到过的一些orm的缺点,那就是没有更新自增列的值.

     在我查阅了一些资料之后先是使用了 输出参数的方式,在insert 语句后面添加一个输出参数用SCOPE_IDENTITY 来取自增值;但是这样做了之后再最后的测试时才发现了问题,Access不支持输出参数,所以真是让人懊恼!

因为这个问题,无奈第2此修改IDataProvider接口,因为我打算用事务来获取自增列值,具体的方法为:

     /// <summary>

         ///  用于执行INSERT语句 并返回自增字段值
        
///   </summary>
        
///   <param name="tablename"> 表名 </param>
        
///   <param name="query"> SQL语句 </param>
        
///   <param name="parameters"> 参数 </param>
        
///   <returns> 自增列值 </returns>
         public   int  ExecuteForIdentity( string  tablename,  string  query,  params  IDataParameter[] parameters)
        {
            
using  (var wapper  =  GetWapperConnection())
            {
                
using  (DbCommand command  =  PrepareCommand(wapper.Connection, query, parameters))
                {
                    
try {
                        DbTransaction transaction 
=  wapper.Connection.BeginTransaction();
                        command.Transaction 
=  transaction;
                        
if  (command.ExecuteNonQuery()  >   0 )
                        {
                            command.CommandText 
=   " SELECT  @@IDENTITY FROM  "   +  tablename;
                            transaction.Commit();
                            
int  id  =  Convert.ToInt32(command.ExecuteScalar());
                            
return  id;
                        }
                        
else
                            
return   0 ;
                    }
                    
catch
                    {
                        
return   - 1 ;
                    }
                   
                }
            }

 

       所以增加了一个int ExecuteForIdentity方法供Save时使用, 这样算是简单的解决了自增列的问题,但是还面临ORACLE数据库取自增值的问题,所以这里我权衡了下,自己对ORACLE了解的也比较少,自己的项目中也不会用到ORACLE,所以干脆就屏蔽掉了对ORACLE数据库的考虑!最终使我这个orm只支持了accesssql server

 

5,  还有一个问题也是在测试的时候发现的,与access数据库有关的

     当我们执行一句形如 :update [Table](age,name) values(@age,@name) where id=@id  这样的一句SQL语句是不会有问题的,但是此时如果你要将@age @name @id 的参数顺序搞错了,那可就不一定了 一个“标准表达式中数据类型不匹配”的异常就让我在这吃了一次亏,还好后来查到了问题原因, 是因为我在添加DbCommand的参数的时候是按照反射来的属性顺序添加的@id就被添加到了第一个,造成了这种access数据库的异常,实际上where查询后的参数应该放在后面,到这里基本上的问题都被处理掉了,我的orm也算是成型了。

 

 

最后就是测试了,当然测试的基础就是按照一开始自己构思的接口,如何使用这个orm,下面我就贴出一段测试代码: 

 

 

TestAccess
         private   static   void  TestAccess()
        {
            
string  conn  =   " Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\db.mdb; " ;
            IDataProvider dbp 
=  DbHelper.CreateDataProvider(ProviderType.ACCESS, conn);
            
if  (dbp.ConnectAvailable  ==   false )
            {
                Console.WriteLine(
" 数据库连接不可用 " );
                Console.ReadLine();
                
return ;
            }
            helper 
=   new  EntityHelper(dbp);
            helper.DebugSql 
+=   new  EventHandler < DebugSqlArgs > ((obj, e)  =>  Console.WriteLine(e.Context));

            XUser user 
=   new  XUser()
            {
                Name 
=   " x " ,
                Age 
=   - 1
            };
            
for  ( int  i  =   0 ; i  <   10 ++ i)
            {
                
bool  delete  =  helper.Delete < XUser > (user);
                Console.WriteLine(
" Delete={0} " , delete);

                
bool  insert  =  helper.Insert < XUser > (user);
                Console.WriteLine(
" Insert={0},XUser.id={1} " , insert, user.Id);

                user.Age 
=  i  +   10 ;
                
bool  update  =  helper.Update < XUser > (user);
                Console.WriteLine(
" Update={0} " , update);
            }
            IList
< XUser >  users  =  helper.Select < XUser > ();
            
foreach  (var iuser  in  users)
            {
                Console.WriteLine(
" IUser.Id={0},Name={1},Age={2} " , iuser.Id, iuser.Name, iuser.Age);
            }
        }

 

 

 

 

TestSql
  private   static   void  TestSql()
        {

            
string  conn  =   " Data Source=localhost,8306;Initial Catalog=SensorLeadDB2;User ID=sa;Password=HZ.bridge " ;
            IDataProvider dbp 
=  DbHelper.CreateDataProvider(ProviderType.SQLSERVER, conn);
            
if  (dbp.ConnectAvailable  ==   false )
            {
                Console.WriteLine(
" 数据库连接不可用 " );
                Console.ReadLine();
                
return ;
            }

            helper 
=   new  EntityHelper(dbp);
            helper.DebugSql 
+=   new  EventHandler < DebugSqlArgs > ((obj, e)  =>  Console.WriteLine(e.Context));

            UserInfo user 
=   new  UserInfo()
            {
                Name 
=   " hello " ,
                Age 
=   0 ,
                Marry 
=   false ,
                Brithday 
=  DateTime.Now,
                Phone 
=   " 000 "
            };
            
for  ( int  i  =   0 ; i  <   10 ++ i)
            {
                
bool  delete  =  helper.Delete < UserInfo > (user);
                Console.WriteLine(
" Delete={0} " , delete);

                
bool  insert  =  helper.Insert < UserInfo > (user);
                Console.WriteLine(
" Insert={0},User.Id={1} " , insert, user.Id);

                user.Age 
=  i  +   10 ;
                
bool  update  =  helper.Update < UserInfo > (user);
                Console.WriteLine(
" Update={0} " , update);
            }

            IList
< UserInfo >  users  =  helper.Select < UserInfo > ( new  QueryCompare(CompareType.Equal, " Marry " , false ),
                
new  QueryOrder( " Age " ));
            
foreach  (var iuser  in  users)
            {
                Console.WriteLine(
" iUser.Id={0},Name={1},Age={2} " , iuser.Id, iuser.Name, iuser.Age);
            }
        }

 

 

 

     经过测试与完善,最后自己还是比较满意的,主要在于是自己思考动手的结果,在这里呢,也将这个简单过程记录下来,希望留给其他需要的朋友一些帮助。 

   到这里发现程序员用语言来描述思维真的是比较困难,所以照例我会上传项目源码 ,方便需要的朋友分析,也希望能得和大家共同交流学习。

 

/Files/cxwx/Bridge.Data.rar 

 

 

 

 

     

 

 

 

 

你可能感兴趣的:(orm)