简单ORM工具的设计和编写,自己项目中曾经用过的
在之前的一个项目中自己编写了一个简单的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语句中去。
例如如下代码:
new UserBriefEntity { Id = Guid.NewGuid(), Age = 111, Birthday = new DateTime(1980, 12, 23) });
我们new了一个UserBriefEntity对象,对三个属性进行了赋值,那么自动生成的SQL语句如下:
注意,本工具生成的SQL语句都是参数化的,所以不存在SQL注入漏洞。
如下代码和自动生成的SQL语句
new UserBriefEntity { Id = Guid.NewGuid(), Name = "wanglx", UserName = "wlx" });
使用 UserEntity 类也没有问题,示例代码和自动生成的SQL语句如下:
new UserEntity { Id = Guid.NewGuid(), Age = 112, Birthday = new DateTime(1981, 1, 11), SN = 12034012, Pwd = "111111", Sex = false });
new UserEntity { Id = Guid.NewGuid(), ChildrenNumber = 2, Desc = "哈哈", Grade = Grade.Normal, SN = 1234 });
Insert方法的返回值是插入数据的条数。
删除:
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)判断。
示例代码如下:
自动生成的SQL语句如下:
注:上面的1=1条件仅仅是当无条件的时候语句仍旧不会报错的处理方式(相信很多人都这样写过),当然也可以在没条件时把where关键字去掉,这里为了方便加了1=1条件。
如果要查询整个表的数据可以这样做:
自动生成的SQL语句如下:
上述方法的泛型参数可以是UserEntity,也可以是UserBriefEntity,只要类中的属性在查询的结果集中有对应的字段就可以(查询结果集中的字段如果在类中无属性对应则忽略)。
结论:select的SQL语句估计仅有10%-20%可以自动生成,仅当对单张的表的字段的相等查询的情况可以自动生成。
以上的增删改查是主要的几个API方法,所有的SQL语句均为自动生成。除了上述几个方法之外还定义了以下几个API方法:
delete操作我们经常会遇到批量删除的情况,比如用户通过多选批量删除数据,本程序定义了通过主键批量删除数据的方法,签名如下:
泛型参数TE是删除的实体(需要通过该实体知道对哪张进行删除操作),泛型删除TK是主键字段对应的实体属性的具体类型。
示例代码如下:
list1是前述中查询得到的结果集,见本文前面所述。
该方法调用自动产生的SQL语句如下:
参数的个数有数组TK[] ids中的个数决定。
如果没有此批量删除方法也可以通过循环调用之前的Delete方法删除,只是效率不高而已,当然此批量删除方法也仅仅是针对主键的批量删除。
update也定义了通过主键批量更新的方法,签名如下:
泛型参数TE定义了更新的实体,TK定义了实体的主键属性类型。
示例代码如下:
list1是前述中查询得到的结果集,见本文前面所述。
自动生成的SQL语句如下:
条件中的参数个数有TK[] ids数组中的元素个数决定。
对于查询操作,本程序定义了多个可以自定义SQL语句的重载函数,最主要的一个API如下:
泛型参数T是实际返回的集合中的实体类型,该实体类型中的需要映射的属性必须包含在查询结果中(结果中的字段值可以为null),TW定义了另一个实体,该实体中的非null属性可以作为查询的参数。parms参数可以自己传入。最终的SQL语句的参数是parms中的参数和TW 实体中非null属性组成的参数的和。
当然自定义的SQL语句可以没有参数,也可以仅有来自TW实体的参数,也可以仅有来自parms的参数,所以,该方法又定义了如下重载方法:
{
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.查询语句无参数的情况
2.查询语句带有两个参数,这两个参数没有手动传入,而是通过传入一个UserEntity实体类型取得,这个实体类型实例必须包含这个两个参数对应的属性,而且属性值必须为非null,如下所示:
"select * from T_USER where AGE>=@AGE AND SEX=@SEX", CommandType.Text, new UserEntity { Age = 22, Sex = true });
可以看出,得到的结果集的实体类型和取参数的实体类型可以不同,总而言之,只要非null属性能够包含相同参数名称就可以。
注意:自定义的SQL语句中的参数命名,默认和字段名称一致。因为从实体实例中自动获取参数就是这个默认的命名规则。
3.两个表的情况:返回结果集实体是OrderEntity类型,而取参数的实体类型是UserEntity
"select * from ORDERENTITY where USER_ID in (select ID from T_USER where REALNAME=@REALNAME)", CommandType.Text, new UserEntity { Name = "Zhezhe1" });
4.自己传入参数的情况
"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.自己传入参数和从实体实例中获取参数相结合
"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查询得到结果,然后再执行批量删除和批量更新操作。
以上只是一个大概介绍和设计思想,具体代码实现会在下一篇中讨论,并且会提供代码下载。