简单ORM工具的设计和编写,自己项目中曾经用过的

http://www.cnblogs.com/szp1118/archive/2011/03/30/ORM.html

在之前的一个项目中自己编写了一个简单的ORM小工具,这次重新整理和重构了一下代码,之所以说简单是因为该小工具仅仅实现了增删改查的简单功能,不具备数据缓存,延迟加载,关联操作等高级功能。正因为简单所以用起来也不麻烦,代码也不是很复杂,但是在数据层至少可以减少70%以上的代码编写量,可以减少至少50%以上的SQL语句编写量。

  

设计思想:实体类中的非null属性都会作为SQL语句中的参数进行处理(在增删改查的SQL语句中的位置有所不同)

 

接下来就从这个设计思想入手:

先来看一下两个简单的类和一张数据库表:

 

 

 

类UserEntity继承于UserBriefEntity,当然只定义一个UserEntity类也可以,之所以定义两个类,是因为在列表中等某些场合比较适合使用字段较少的UserBriefEntity类,而在详细信息显示等场合适合使用包含全部字段的UserEntity类。

 

 

设计思路:

1.数据库中的一张表需要和实体类进行对应,表中的字段对应到实体类的属性,不一定都要一一对应,可以有属性不对应到表中,但是一般情况下数据库字段都应该对应到某个属性,否则就没法对该字段进行操作了。有时候在列表页和详细页需要获取的数据不同(列表页是少数几个字段),这种情况下可以定义一个少量字段的实体类,再定义一个所有字段的实体类(继承至前一个类)。

2.由于.net的基本类型例如int,bool等都是值类型,意味着无法赋值为null,它们都有默认初始值,int为0,bool为false,这样的话自动处理就无法分辨出是默认赋值还是用户自己赋值,所以实体类中的属性就必须是引用类型,对于.net基本类型就要做可空处理了(.net 2.0新增功能),如 int? , bool? 这样来定义实体类的属性类型。

 

增删改查的具体处理:

 新增:

insert操作都是对一张表进行的,对一张表的新增无非就是SQL语句中字段的多少问题了,这个可以通过自动生成SQL语句来完成,新增操作的SQL语句应该说100%可以自动生成,生成SQL语句的原则是实体类中非null值的属性都作为需要新增的字段进行处理,而值为null的属性不会生成到SQL语句中去。

例如如下代码:

 Zhezhe.Common.DataAccess.EntityHelper.Insert(
                new UserBriefEntity { Id = Guid.NewGuid(), Age = 111, Birthday = new DateTime(1980, 12, 23) });

我们new了一个UserBriefEntity对象,对三个属性进行了赋值,那么自动生成的SQL语句如下:

INSERT INTO T_USER (Id,Age,Birthday) VALUES (@Id,@Age,@Birthday)

注意,本工具生成的SQL语句都是参数化的,所以不存在SQL注入漏洞。

 

如下代码和自动生成的SQL语句

Zhezhe.Common.DataAccess.EntityHelper.Insert(
                new UserBriefEntity { Id = Guid.NewGuid(), Name = "wanglx", UserName = "wlx" });
INSERT INTO T_USER (Id,UserName,REALNAME) VALUES (@Id,@UserName,@REALNAME)

 

使用 UserEntity 类也没有问题,示例代码和自动生成的SQL语句如下:

Zhezhe.Common.DataAccess.EntityHelper.Insert(
                new UserEntity { Id = Guid.NewGuid(), Age = 112, Birthday = new DateTime(1981, 1, 11), SN = 12034012, Pwd = "111111", Sex = false });
INSERT INTO T_USER (PASSWORD,Sex,SN,Id,Age,Birthday) VALUES (@PASSWORD,@Sex,@SN,@Id,@Age,@Birthday)

 

Zhezhe.Common.DataAccess.EntityHelper.Insert(
                new UserEntity { Id = Guid.NewGuid(), ChildrenNumber = 2, Desc = "哈哈", Grade = Grade.Normal, SN = 1234 });
INSERT INTO T_USER (DESCRIPTION,Grade,ChildrenNumber,SN,Id) VALUES (@DESCRIPTION,@Grade,@ChildrenNumber,@SN,@Id)

Insert方法的返回值是插入数据的条数。

 

 从以上示例代码可以看出,自动生成insert语句的原则完全是按照“实体类中的非null属性都会作为SQL语句中的参数进行处理”这一设计思想展开的,只要是非null属性都会被拼接到SQL语句中去。
 
Insert方法签名如下:
public static int Insert<T>(T instance)
 
 结论:insert的SQL语句100%可以自动生成
 

删除:

delete操作也是对一张表进行的,删除操作比较复杂,主要是where条件的不同,自动生成的SQL语句不可能做到能生成各种复杂条件,这里如果要设计复杂的话就会很复杂了,还是简单化一些吧,本工具只自动生成实体类中非null属性作为where条件“和”相等性判断的SQL语句,具体来说就是,如果实体类中所有属性都是null,那么生成的DELETE SQL语句就不会带有任何条件了,这样整个表的数据就被删除了,如果实体中A属性的值为”a”,其余属性值为null,则生成的SQL语句中 where 条件为 A = “a” ,如果A属性值为”a”B属性值为”b”,则相应的where条件为A = “a” and B=”b”,自动生成的DELETE语句中无或(or)判断。

 
 示例代码如下:
Zhezhe.Common.DataAccess.EntityHelper.Delete(new UserEntity() { Age = 21, Grade = Grade.Diamond });
 自动生成的SQL语句如下:
DELETE FROM T_USER  WHERE  Grade=@Grade  AND  Age=@Age 
 从以上示例代码可以看出,自动生成delete语句的原则也完全是按照“实体类中的非null属性都会作为SQL语句中的参数进行处理”这一设计思想展开的,只要是非null属性都会被拼接到SQL语句的where条件中去。在拼接的过程中收到以下限制:1.条件只能为=号,2.条件之间只能是and关系。之所以按照这么个逻辑生成因为是这种方式最常用。
 结论:delete的SQL语句50%可以自动生成,如果结合下面的先查询再删除的方法进行的话则可以做到90%的delete语句可以自动生成,无需手写。
Delete方法的签名如下:
 public static int Delete<T>(T instance)
 
修改:
update操作也是对一张表进行的,update操作相比较insert和delete更为复杂,insert语句的所有参数都位于同一个位置(values 后面),delete语句的所有参数也都位于同一个位置(where 后面),而update语句的参数位置则有两个地方,一个是set后面,另一个是where后面,实体类中的非null属性如果作为SQL语句参数的话就不知道是放在这两个位置的哪个位置了,这里作了如下的处理:主键属性作为where条件,其它的属性都作为set语句,也就意味着自动生成的update语句只能根据主键字段作为条件进行更新操作。
示例代码如下:
Zhezhe.Common.DataAccess.EntityHelper.Update(new UserEntity { Id = list3[0].Id, Birthday = new DateTime(1985, 1, 4), Desc = "我被修改过了" });
自动生成的update语句如下:
UPDATE T_USER SET   DESCRIPTION=@DESCRIPTION  , Birthday=@Birthday  WHERE Id=@Id
注意:这里UserEntity实体的主键字段属性必须赋值,否则会报异常(今后可能会修改为主键字段属性如果null,则更新整个表的数据)。
 
结论:update的SQL语句30%可以自动生成,如果结合下面的先查询再更新的方法的话则可以做到80%的delete语句可以自动生成,无需手写。
Update方法的签名如下:
public static int Update<T>(T instance)
 
查询:
select是最复杂的,以上的增删改都是针对一张表进行的,而且返回值都是整型类型的受影响的行数,而select则可能针对几张表操作,返回值是一个结果集,如果使用ADO.NET的话则需要存放至DataTable,或者是通过DataReader赋值给对象。本程序中进行select操作的方法为GetList方法,具有多个重载方法。本程序中将返回的结果集全部转换为对象的集合,都是通过DataReader的方式进行的,没有使用DataTable。结果集中的字段列表也需要相应的实体对应。本程序中仅对一种情况自动生成select SQL语句:对单张表(视图)查询,条件为非null属性,条件仅为相等,各条件之间仅为and连接,整个delete语句的条件生成方式是一样的。
方法签名如下:
public static IList<T> GetList<T>(T instance)

示例代码如下:

 

var list1 = Zhezhe.Common.DataAccess.EntityHelper.GetList<UserEntity>(new UserEntity() { Age = 20 });

自动生成的SQL语句如下:

SELECT * FROM T_USER WHERE 1=1  AND  Age=@Age 

注:上面的1=1条件仅仅是当无条件的时候语句仍旧不会报错的处理方式(相信很多人都这样写过),当然也可以在没条件时把where关键字去掉,这里为了方便加了1=1条件。

如果要查询整个表的数据可以这样做:

var list1_1 = Zhezhe.Common.DataAccess.EntityHelper.GetList<UserEntity>(new UserEntity() { });

自动生成的SQL语句如下:

SELECT * FROM T_USER WHERE 1=1

 

上述方法的泛型参数可以是UserEntity,也可以是UserBriefEntity,只要类中的属性在查询的结果集中有对应的字段就可以(查询结果集中的字段如果在类中无属性对应则忽略)。

结论:select的SQL语句估计仅有10%-20%可以自动生成,仅当对单张的表的字段的相等查询的情况可以自动生成。

 

以上的增删改查是主要的几个API方法,所有的SQL语句均为自动生成。除了上述几个方法之外还定义了以下几个API方法:

 

delete操作我们经常会遇到批量删除的情况,比如用户通过多选批量删除数据,本程序定义了通过主键批量删除数据的方法,签名如下:

public static int BatchDelete<TE, TK>(TK[] ids)

 

泛型参数TE是删除的实体(需要通过该实体知道对哪张进行删除操作),泛型删除TK是主键字段对应的实体属性的具体类型。

 

示例代码如下:

 

Zhezhe.Common.DataAccess.EntityHelper.BatchDelete<UserEntity,Guid?>(list1.Select(e => e.Id).ToArray());

list1是前述中查询得到的结果集,见本文前面所述。

该方法调用自动产生的SQL语句如下:

DELETE FROM T_USER WHERE   Id=@P_ID_0  OR  Id=@P_ID_1  OR  Id=@P_ID_2  OR  Id=@P_ID_3  OR  Id=@P_ID_4  OR  Id=@P_ID_5  OR  Id=@P_ID_6 

参数的个数有数组TK[] ids中的个数决定。

如果没有此批量删除方法也可以通过循环调用之前的Delete方法删除,只是效率不高而已,当然此批量删除方法也仅仅是针对主键的批量删除。

 

 update也定义了通过主键批量更新的方法,签名如下:

public static int BatchUpdate<TE, TK>(TK[] ids, TE instance)

泛型参数TE定义了更新的实体,TK定义了实体的主键属性类型。

示例代码如下:

Zhezhe.Common.DataAccess.EntityHelper.BatchUpdate(list1.Select(e => e.Id).ToArray(), new UserEntity { QQ = "2222222", Name = "改名了" });

list1是前述中查询得到的结果集,见本文前面所述。

自动生成的SQL语句如下:

UPDATE T_USER SET   QQ=@QQ  , REALNAME=@REALNAME  WHERE      Id=@P_ID_0  OR  Id=@P_ID_1  OR  Id=@P_ID_2  OR  Id=@P_ID_3

条件中的参数个数有TK[] ids数组中的元素个数决定。

 

对于查询操作,本程序定义了多个可以自定义SQL语句的重载函数,最主要的一个API如下:

public static IList<T> GetList<T, TW>(string sql, SqlParameter[] parms, CommandType ct, TW instance)

泛型参数T是实际返回的集合中的实体类型,该实体类型中的需要映射的属性必须包含在查询结果中(结果中的字段值可以为null),TW定义了另一个实体,该实体中的非null属性可以作为查询的参数。parms参数可以自己传入。最终的SQL语句的参数是parms中的参数和TW 实体中非null属性组成的参数的和。

 当然自定义的SQL语句可以没有参数,也可以仅有来自TW实体的参数,也可以仅有来自parms的参数,所以,该方法又定义了如下重载方法:

 
 public static IList<T> GetList<T>(string sql, SqlParameter[] parms, CommandType ct)
        {
            return EntityHelper.GetList<T, Util.NoPropertyClass>(sql, parms, ct, new Util.NoPropertyClass());
        }

        public static IList<T> GetList<T>(string sql, SqlParameter[] parms, CommandType ct, T instance)
        {
            return EntityHelper.GetList<T, T>(sql, parms, ct, instance);
        }

        public static IList<T> GetList<T, TW>(string sql, CommandType ct, TW instance)
        {
            return EntityHelper.GetList<T, TW>(sql, null, ct, instance);
        }

        public static IList<T> GetList<T>(string sql, CommandType ct, T instance)
        {
            return EntityHelper.GetList<T, T>(sql, null, ct, instance);
        }

        public static IList<T> GetList<T>(string sql, CommandType ct) where T:class
        {
            return EntityHelper.GetList<T>(sql, null, ct, null);
        }
 

 示例代码如下:

1.查询语句无参数的情况

 var lsit4 = Zhezhe.Common.DataAccess.EntityHelper.GetList<UserBriefEntity>("select * from T_USER where AGE>=21 AND SEX=1", CommandType.Text);

2.查询语句带有两个参数,这两个参数没有手动传入,而是通过传入一个UserEntity实体类型取得,这个实体类型实例必须包含这个两个参数对应的属性,而且属性值必须为非null,如下所示:

var list5 = Zhezhe.Common.DataAccess.EntityHelper.GetList<UserBriefEntity, UserEntity>(
                "select * from T_USER where AGE>=@AGE AND SEX=@SEX", CommandType.Text, new UserEntity { Age = 22, Sex = true });

可以看出,得到的结果集的实体类型和取参数的实体类型可以不同,总而言之,只要非null属性能够包含相同参数名称就可以。

注意:自定义的SQL语句中的参数命名,默认和字段名称一致。因为从实体实例中自动获取参数就是这个默认的命名规则。

3.两个表的情况:返回结果集实体是OrderEntity类型,而取参数的实体类型是UserEntity

 var list6 = Zhezhe.Common.DataAccess.EntityHelper.GetList<OrderEntity,UserEntity>(
               "select * from ORDERENTITY where USER_ID in (select ID from T_USER where REALNAME=@REALNAME)", CommandType.Text, new UserEntity { Name = "Zhezhe1" });

4.自己传入参数的情况

var list7 = Zhezhe.Common.DataAccess.EntityHelper.GetList<OrderEntity>(
               "select * from ORDERENTITY where USER_ID in (select ID from T_USER where REALNAME=@MY_REALNAME)", new SqlParameter[] { new SqlParameter("@MY_REALNAME", "Zhezhe1") }, CommandType.Text);

由于是自己传入参数,所以参数的命名可以随意自己取名,这里取名为@MY_REALNAME

5.自己传入参数和从实体实例中获取参数相结合

var list8 = Zhezhe.Common.DataAccess.EntityHelper.GetList<OrderEntity,UserEntity>(
               "select * from ORDERENTITY where USER_ID in (select ID from T_USER where REALNAME=@MY_REALNAME AND AGE=@AGE)", new SqlParameter[] { new SqlParameter("@MY_REALNAME", "Zhezhe1") }, CommandType.Text, new UserEntity { Age = 22 });

参数@MY_REALNAME来自自己传入,参数@AGE从实体中获得

 

以上的select的结果的对应实体可以自己随意定义,只要能和select结果中的字段对应即可,不一定要和表对应,因为select的结果可能来自多个表。

 

总结:虽然自定义的SQL语句需要自己手写SQL,但是基本可以处理所有各种复杂查询,这些API主要在以下几点给您省时省力,1.参数可以通过实体实例取得,不一定都要自己传入,免去了大量的定义参数的代码,2.返回的结果程序自动转换为对应实体的集合,免去了大量的DataReader取数据赋值的代码。

 

前文曾经提到删除和修改操作可以通过先查询后执行删除或修改的办法来实现。 可以首先通过自定义的SQL查询得到结果,然后再执行批量删除和批量更新操作。

 

以上只是一个大概介绍和设计思想,具体代码实现会在下一篇中讨论,并且会提供代码下载。

posted @ 2015-07-08 22:08  awp110  阅读(222)  评论(1编辑  收藏  举报