最近,我看到园子里面有位朋友的一篇博客 《领域驱动设计系列(一):为什么要领域驱动设计? 》文章中有下面一段话,对DDD使用产生的疑问:html
•没有正确的使用ORM, 致使数据加载过多,致使系统性能不好。 •为了解决性能问题,就不加载一些导航属性,可是却把DB Entity返回上层,这样对象的一些属性为空,上层使用这个数据时根本不知道什么时间这个属性是有值的,这个是很丑陋的是否是?
博主说的第一个问题,是由于使用ORM的人把实体类的所有属性的数据查询出来了,至关于执行了 select * from table 这样的查询,而实际上,Domain层是不须要这么多额外的数据的。sql
从新定义一个Domain须要的 DTO? 但这又会致使DTO膨胀,DTO对象满天飞!数据库
因此为了简便,就直接查询出所有属性对应的数据,或者也用EF的Select子句,投影下,但将结果又投影给了另一个DTO对象或者Entity 对象,这样就使得对象中部分属性为空了,因而又产生了博主的第二个问题。数组
第二个问题有多严重?缓存
假设某个表有50个字段,这样大的表在不少复杂的系统中是很常见的,因而MAP出来的Entity或者DTO,也有50个属性,而我此次仅须要使用其中的2个属性的值,因而,这个对象上的 48个属性数据都浪费了。安全
若是这样的DTO对象用在List上且用于分布式环境,那么,这样浪费的网络IO和序列化,凡序列化浪费的CPU,仍是比较严重的。服务器
好比有下面一个用户信息类接口:网络
public interface IUser { int Age { get; set; } string FirstName { get; set; } string LasttName { get; set; } int UserID { get; set; } }
而后根据这个接口,写一个PDF.NET SOD 实体类 UserEntity ,用于持久化数据到数据库或者其它用途:数据结构
public class UserEntity:EntityBase, IUser { public UserEntity() { TableName = "Users"; IdentityName = "User ID"; PrimaryKeys.Add("User ID"); } public int UserID { get { return getProperty<int>("User ID"); } set { setProperty("User ID", value); } } public string FirstName { get { return getProperty<string>("First Name"); } set { setProperty("First Name", value,20); } } public string LasttName { get { return getProperty<string>("Last Name"); } set { setProperty("Last Name", value,10); } } public int Age { get { return getProperty<int>("Age"); } set { setProperty("Age", value); } } }
还有一个用户类的DTO类 UserDto,可用于分布式系统的数据传输或者解决方案多个项目分层之间的数据传输:架构
public class UserDto:IUser { public int Age { get; set; } public string FirstName { get; set; } public string LasttName { get; set; } public int UserID { get; set; } }
若是 UserEntity user=new UserEntity();此时user 对象里面并无 UserID 的数据,除非调用了属性的Set方法,此时,能够用下面的代码来验证:
UserEntity user=new UserEntity(); bool flag=(user["User ID"] ==null);//true
注意 user["User ID"] 这个地方,SOD的实体类能够看成“索引器”来使用,索引器的Key是实体类属性Map的数据库字段名称,请看UserEntity. UserID 属性的定义:
public int UserID { get { return getProperty<int>("User ID"); } set { setProperty("User ID", value); } }
可见咱们能够将一个不一样的字段名影射到一个属性名上。因此,根据这个定义,访问索引器 user["User ID"] 就等于访问 user实体类的属性 UserID 。
从这里咱们能够得出结论:
结论一:
SOD 实体类的属性值默认均为空 (null)
此时的空,表明数据没有做任何初始化,这种“空”来自以程序中。咱们还能够经过查询来进一步验证这种状况的空值:
假如咱们的ORM查询语言OQL查询并无指定要查询实体类的Age属性,那么结果user对象仅有2个数据,并无3个数据:
OQL q3 = OQL.From(uq)
.Select(uq.UserID, uq.FirstName) //未查询 user.Age 字段 .Where(uq.FirstName) .END; UserEntity user3 = context.UserQuery.GetObject(q3); //未查询 user.Age 字段,此时查询该字段的值应该是 null bool flag3 = (user3["Age"] == null);//true Console.WriteLine("user[\"Age\"] == null :{0}", flag); Console.WriteLine("user.Age:{0}", user3.Age);
程序输出:
user["Age"] == null :True user.Age:0
为了验证SOD 实体类从数据库查询出来的字段的空值是什么状况,咱们先插入几条测试数据:
LocalDbContext context = new LocalDbContext();//自动建立表 //插入几条测试数据 context.Add<UserEntity>(new UserEntity() { FirstName ="zhang", LasttName="san" }); context.Add<IUser>(new UserDto() { FirstName = "li", LasttName = "si", Age = 21 }); context.Add<IUser>(new UserEntity() { FirstName = "wang", LasttName = "wu", Age = 22 });
咱们插入的第一条数据并无年龄Age 的数据,下面再来查询这条数据,看数据库的值是否为NULL:
//查找姓张的一个用户 UserEntity uq = new UserEntity() { FirstName = "zhang" }; OQL q = OQL.From(uq) .Select(uq.UserID, uq.FirstName, uq.Age) .Where(uq.FirstName) .END; //下面的语句等效 //UserEntity user2 = EntityQuery<UserEntity>.QueryObject(q,context.CurrentDataBase); UserEntity user2 = context.UserQuery.GetObject(q); //zhang san 的Age 未插入值,此时查询该字段的值应该是 NULL bool flag2 = (user2["Age"] == DBNull.Value);//true Console.WriteLine("user[\"Age\"] == DBNULL.Value :{0}", flag);
注意,这里咱们在OQL的Select 子句中,指定了要查询实体类的 Age 属性,若是数据库没有该属性字段的值,它必定是NULL,也就是 程序中说的 NBNULL.Value,看输出结果验证:
user["Age"] == DBNULL.Value :True user.Age:0
固然,这里数据库为空,要求表字段是支持可空的。
从这里咱们能够得出结论:
结论二: SOD 用OQL 查询的实体类属性,若是数据库对应的字段值为空,那么实体类内部该属性值也为空(DBNull.Value)
在OQLCompare对象上,能够直接调用 IsNull 方法来判断实体类某个属性在数据库对应的值是否为空,例以下面的例子:
//查询没有填写 LastName的用户,即LastName==DBNull.Value; UserEntity uq = new UserEntity() ; OQL q = OQL.From(uq) .Select(uq.UserID, uq.FirstName, uq.Age) .Where(cmp => cmp.IsNull( uq.LastName)) .END;
将输出下面的SQL:
Select [UserID],[FistName],[Age] From [User] Where [LastName] IS NULL
在EF等ORM中,要定义一个字段可空,须要定义成可空类型,好比咱们的User类,假设定义成EF的实体类,应该是这样子的:
public class EFUserEntity { int? Age { get; set; } [MaxLength(20)] string? FirstName { get; set; } [MaxLength(10)] string? LasttName { get; set; } [Key] [Required] int UserID { get; set; } //主键,不可为空 }
这种可空类型的实体类定义,可以让数据库字段标记为NULL,可是,这个实体类在于DTO类进行转换的时候,总会遇到一些麻烦,由于实体类属性为空,而DTO属性不为空。
有人说,咱们把DTO属性也定义为可空类型,不就行了么?
我在想,.NET推出值类型上的可空类型,本意是为了兼容从数据库来的空值,这样,对于 int a; 这个变量来讲,能够知道它的值究竟是0,仍是变量根本没有值,这是未知的,而int? a; 这个变量完美的解决了这个问题。
可是,若是你的服务的客户端不是.net,而是JAVA,JS,或者其它不支持可空类型的语言,这种有可空类型属性的DTO就赶上麻烦了。
因此,SOD的实体类,属性能够定义为非可空类型的,可是属性的内部值,null或者 DBNull.Value 都是能够的。
SOD实体类能够仅看做一个数据容器,又能够看做一个ORM的实体类,大大增长了使用的灵活性和查询的效率。
对于上面的查询,无论Age属性在实体类里面是
bool flag=(user2["Age"]==NBNull.Value);//true
仍是
bool flag=(user3["Age"]==null);//true
当外面获取Age属性的时候,都是Age的默认值0:
int age=user2.Age;//0 int age=user3.Age;//0
这些数据在实体类中是怎么存储的呢?原来,实体类内部有一个相似于“名-值对”的2个数组,用于存储实体类映射的数据库字段名和字段的值,这个结构就是SOD框架的中的 PropertyNameValues 类,定义很简单:
public class PropertyNameValues { public string[] PropertyNames { get; set; } public object[] PropertyValues { get; set; } }
因此实体类的字段值是存储在Object对象上,这也是 为什么SOD实体类能够处理2种空值null,DBNull.Value的缘由。固然你也能够存其它内容,只要属性类型兼容便可。好比属性类型是long,而数据库字段的值类型是 int ,这在SOD实体类是容许的。
下面这个查询,动态查询一个实体类的属性是否等于指定的值,或者该属性对应的字段在数据库是否为空,而实现动态查询的关键,是使用索引器,
以下面的BatchNumber 属性,查询此属性值是否为0或者是否为空:
private OQL FilterQuery(EntityBase entity) { if (entity is IExportTable) { entity["BatchNumber"] = 0; OQL q = OQL.From(entity) .Select() .Where(cmp => cmp.EqualValue(entity["BatchNumber"]) | cmp.IsNull(entity["BatchNumber"])) .END; return q; } return null; }
另外,这个值的可变性,使得SOD框架处理 枚举属性 很是方便,由于,Enum 与int 类型是兼容的,能够相互转换,参看这篇文章:
《 实体类的枚举属性--原来支持枚举类型这么简单,没有EF5.0也能够》
属性值的可变性,除了上面的好处,还有什么好处?
好处大大的,这意味着 PropertyNames,PropertyValues 的长度是可变的,就像前面的例子,查询了Age属性,实体类的值有3个,而不查询,那么值只有2个。
假设实体类有50个属性,本次只查询了2个属性,那么SOD的实体类实际传输的数据就只有2个,而不是50个,这将大大节省数据传输量。
这个能够经过SOD实体类的序列化结果来验证。
这里必然绕不开实体类的序列化与反序列化,如今最新的SOD框架已经内置支持,参考下面的代码:
//查找姓张的一个用户 UserEntity uq = new UserEntity() { FirstName = "zhang" }; OQL q3 = OQL.From(uq) .Select(uq.UserID, uq.FirstName) //未查询 user.Age 字段 .Where(uq.FirstName) .END; UserEntity user3 = context.UserQuery.GetObject(q3); Console.WriteLine("实体类序列化测试"); var entityNameValues= user3.GetNameValues(); PropertyNameValuesSerializer ser = new PropertyNameValuesSerializer(entityNameValues); string strEntity = ser.Serializer(); Console.WriteLine(strEntity); Console.WriteLine("成功"); // Console.WriteLine("反序列化测试"); PropertyNameValuesSerializer des = new PropertyNameValuesSerializer(null); UserEntity desUser = des.Deserialize<UserEntity>(strEntity); Console.WriteLine("成功");
下面是序列化结果的输出:
<?xml version="1.0" encoding="utf-16"?> <PropertyNameValues xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <PropertyNames> <string>User ID</string> <string>First Name</string> </PropertyNames> <PropertyValues> <anyType xsi:type="xsd:int">26</anyType> <anyType xsi:type="xsd:string">zhang</anyType> </PropertyValues> </PropertyNameValues>
可见,以这种方式序列化传输的数据量,将是不多的。固然,你还能够更改为JSOn序列化,这样数据更少,缺点是数据元数据没有了。
三层或者多层架构,或者DDD架构,少不了Entity,DomainModel,DTO 之间的数据拷贝,若是数据结构高度类似,可使用AutoMapper之类的工具,而在SOD框架内,使用了速度最快的属性拷贝方案,参见以前我写的博客文章:
《使用反射+缓存+委托,实现一个不一样对象之间同名同类型属性值的快速拷贝》
另外,若是是从实体类到DTO,或者DTO到实体类的数据复制,在EntityBase上提供了 MapFrom和MapTo方法,例以下面使用的例子:
IUser TestMapFromDTO(IUser data)
{ IUser user = EntityBuilder.CreateEntity<IUser>(); ((entityBase)user).MapFrom(data); return user; }
固然,还有CopyTo方法,只要你引用了框架扩展 PWMIS.Core.Extension.dll
using PWMIS.Core.Extensions; ... ... //CoyTo 建立一个实例对象 ImplCarInfo icResult= info.CopyTo<ImplCarInfo>(null); //CopyTo 填充一个实例对象 ImplCarInfo icResult2 = new ImplCarInfo(); info.CopyTo<ImplCarInfo>(icResult2);
将实体类的数据拷贝到DTO对象的时候,推荐下面这种直接调用 这种方式:
DTOXXX dto=EntityObject.CopyTo<DTOXX>();
有不少朋友想在WebService上直接使用SOD实体类,可是因为实体类继承自实体类接口,默认的XML序列化会失败,不过WCF采用了不一样的序列化方式,能够序列化SOD的实体类,可是会将实体类内部的一些数据也序列化过去,增大数据传输量,所以,我通常都是建议在WCF,WebService 的服务方法上使用DTO对象,而不是SOD实体类。能够经过上面的方法实现实体类与DTO之间的转换。
可是,采用DTO对象会致使“数据更新冗余”,好比某个属性没有修改,DTO上也会有对应的默认值的,好比 userEntity.Age 属性,若是从未赋值,那么 userDto.Age 也会有默认值 0 ,而传输这个默认值0 并无意义,而且有可能让服务后段的ORM代码将这个 0 更新到数据库中,这就是数据更新容易。
有时候,咱们但愿只更新已经改变的数据,没有改变的数据不更新,那么此时WCF等服务端的方法,采用DTO对象就没法作到了。幸亏,SOD的实体类提供了仅仅获取更改过的数据的方法,请看下面的例子:
//序列化以后的属性是否修改的状况测试,下面的实体类,LastName 属性没有被修改 UserEntity user4 = new UserEntity() { UserID =100, Age=20, FirstName ="zhang san"}; entityNameValues = user4.GetChangedValues(); PropertyNameValuesSerializer ser = new PropertyNameValuesSerializer(entityNameValues); string strEntity = ser.Serializer(); Console.WriteLine(strEntity); Console.WriteLine("成功"); // Console.WriteLine("反序列化测试"); PropertyNameValuesSerializer des = new PropertyNameValuesSerializer(null); UserEntity desUser = des.Deserialize<UserEntity>(strEntity); Console.WriteLine("成功");
这里须要调用实体类的 GetChangedValues 方法,这样序列化的时候就只序列化了修改过的数据了,而且反序列化以后,数据也还原了以前的“修改状态”,拿这样的实体类去更新数据库,就不会出现“数据更新冗余”了。
下面是一个WCF方法示例:
public void Dosomething(PropertyNameValues para) { UserEntity user = new UserEntity(); PropertyNameValuesSerializer ser = new PropertyNameValuesSerializer(para); ser.FillEntity(user); //To Dosomething..... }
注意:该功能须要SOD框架的 5.2.3.0527 版本以上支持
最新版的SOD框架(PDF.NET SOD)已经能够方便的支持CodeFirst开发了,使用很简单,调用只须要一行代码:
Console.WriteLine("第一次运行,将检查并建立数据表"); LocalDbContext context = new LocalDbContext();//自动建立表
而这个LocalDbContext 的定义也不复杂:
public class LocalDbContext : DbContext // 内部会根据 local 链接字符串名字,决定是否使用 SqlServerDbContext public LocalDbContext() : base("local") { //local 是链接字符串名字 } #region 父类抽象方法的实现 protected override bool CheckAllTableExists() { //建立用户表 CheckTableExists<UserEntity>(); return true; } #endregion }
综合结论:
因此SOD实体类对用户而言是透明的,它并无增长使用的复杂性,又能够很好的控制数据量,还可让你知道数据来自哪里,简单而又强大。
这样的ORM,才是合适DDD的ORM,固然,SOD不只仅是一个ORM,它还有SQL-MAP和DataControl,具体能够看框架官网 http://www.pwmis.com/sqlmap ,9年历史铸就的成果,坚固可靠。
附注:
下面是本文说明中使用的完整代码:
class Program { static void Main(string[] args) { Console.WriteLine("====**************** PDF.NET SOD 控制台测试程序 **************===="); Assembly coreAss = Assembly.GetAssembly(typeof(AdoHelper));//得到引用程序集 Console.WriteLine("框架核心程序集 PWMIS.Core Version:{0}", coreAss.GetName().Version.ToString()); Console.WriteLine(); Console.WriteLine(" 应用程序配置文件默认的数据库配置信息:\r\n 当前使用的数据库类型是:{0}\r\n 链接字符串为:{1}\r\n 请确保数据库服务器和数据库是否有效,\r\n继续请回车,退出请输入字母 Q ." , MyDB.Instance.CurrentDBMSType.ToString(), MyDB.Instance.ConnectionString); Console.WriteLine("=====Power by Bluedoctor,2015.2.10 http://www.pwmis.com/sqlmap ===="); string read = Console.ReadLine(); if (read.ToUpper() == "Q") return; Console.WriteLine(); Console.WriteLine("-------PDF.NET SOD 实体类 测试---------"); //注册实体类 EntityBuilder.RegisterType(typeof(IUser), typeof(UserEntity)); UserEntity user = EntityBuilder.CreateEntity<IUser>() as UserEntity; bool flag = (user["User ID"] == null);//true Console.WriteLine("user[\"User ID\"] == null :{0}",flag); Console.WriteLine("user.UserID:{0}", user.UserID); Console.WriteLine("第一次运行,将检查并建立数据表"); LocalDbContext context = new LocalDbContext();//自动建立表 //删除测试数据 OQL deleteQ = OQL.From(user) .Delete() .Where(cmp=>cmp.Comparer(user.UserID,">",0)) //为了安全,不带Where条件是不会所有删除数据的 .END; context.UserQuery.ExecuteOql(deleteQ); Console.WriteLine("插入3条测试数据"); //插入几条测试数据 context.Add<UserEntity>(new UserEntity() { FirstName ="zhang", LasttName="san" }); context.Add<IUser>(new UserDto() { FirstName = "li", LasttName = "si", Age = 21 }); context.Add<IUser>(new UserEntity() { FirstName = "wang", LasttName = "wu", Age = 22 }); //查找姓张的一个用户 UserEntity uq = new UserEntity() { FirstName = "zhang" }; OQL q = OQL.From(uq) .Select(uq.UserID, uq.FirstName, uq.Age) .Where(uq.FirstName) .END; //下面的语句等效 //UserEntity user2 = EntityQuery<UserEntity>.QueryObject(q,context.CurrentDataBase); UserEntity user2 = context.UserQuery.GetObject(q); //zhang san 的Age 未插入值,此时查询该字段的值应该是 NULL bool flag2 = (user2["Age"] == DBNull.Value);//true Console.WriteLine("user[\"Age\"] == DBNULL.Value :{0}", flag); Console.WriteLine("user.Age:{0}", user2.Age); OQL q3 = OQL.From(uq) .Select(uq.UserID, uq.FirstName) //未查询 user.Age 字段 .Where(uq.FirstName) .END; UserEntity user3 = context.UserQuery.GetObject(q3); //未查询 user.Age 字段,此时查询该字段的值应该是 null bool flag3 = (user3["Age"] == null);//true Console.WriteLine("user[\"Age\"] == null :{0}", flag); Console.WriteLine("user.Age:{0}", user3.Age); Console.WriteLine("实体类序列化测试"); var entityNameValues= user3.GetNameValues(); PropertyNameValuesSerializer ser = new PropertyNameValuesSerializer(entityNameValues); string strEntity = ser.Serializer(); Console.WriteLine(strEntity); Console.WriteLine("成功"); // Console.WriteLine("反序列化测试"); PropertyNameValuesSerializer des = new PropertyNameValuesSerializer(null); UserEntity desUser = des.Deserialize<UserEntity>(strEntity); Console.WriteLine("成功"); Console.WriteLine(); Console.WriteLine("----测试完毕,回车结束-----"); Console.ReadLine(); } }
图片的效果要好些:
有关该测试程序的完整下载和查看,请看框架开源项目地址:
http://pwmis.codeplex.com/SourceControl/latest#SOD/Test/EntityTest-2013/Program.cs
其它: