引言:DDD的困惑
最近,我看到园子里面有位朋友的一篇博客 《领域驱动设计系列(一):为何要领域驱动设计? 》文章中有下面一段话,对DDD使用产生的疑问:
•没有正确的使用ORM, 导致数据加载过多,导致系统性能很差。 •为了解决性能问题,就不加载一些导航属性,但是却把DB Entity返回上层,这样对象的一些属性为空,上层使用这个数据时根本不知道什么时间这个属性是有值的,这个是很丑陋的是不是?
博主说的第一个问题,是因为使用ORM的人把实体类的全部属性的数据查询出来了,相当于执行了 select * from table 这样的查询,而实际上,Domain层是不需要这么多额外的数据的。
重新定义一个Domain需要的 DTO? 但这又会导致DTO膨胀,DTO对象满天飞!
所以为了简便,就直接查询出全部属性对应的数据,或者也用EF的Select子句,投影下,但将结果又投影给了另外一个DTO对象或者Entity 对象,这样就使得对象中部分属性为空了,于是又产生了博主的第二个问题。
第二个问题有多严重?
假设某个表有50个字段,这样大的表在很多复杂的系统中是很常见的,于是MAP出来的Entity或者DTO,也有50个属性,而我这次仅需要使用其中的2个属性的值,于是,这个对象上的 48个属性数据都浪费了。
如果这样的DTO对象用在List上且用于分布式环境,那么,这样浪费的网络IO和序列化,凡序列化浪费的CPU,还是比较严重的。
1,准备工作
比如有下面一个用户信息类接口:
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; } }
2,SOD框架的实体类
2.1,索引器访问与字段映射
如果 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 。
2.2,“空”的两种境界(null / DBNull.Value)
从这里我们可以得出结论:
结论一:
SOD 实体类的属性值默认均为空 (null)
2.2.1,程序中的 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
2.2.2,数据库中的 NULL
为了验证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)
2.2.3 在OQL查询中的NULL
在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
2.3,可空类型的问题
在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 都是可以的。
3,数据的容器
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实体类是允许的。
3.1,综合示例
下面这个查询,动态查询一个实体类的属性是否等于指定的值,或者该属性对应的字段在数据库是否为空,而实现动态查询的关键,是使用索引器,
如下面的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实体类的序列化结果来验证。
4,在分布式系统上使用实体类
4.1,实体类的序列化与反序列化
这里必然绕不开实体类的序列化与反序列化,现在最新的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序列化,这样数据更少,缺点是数据元数据没有了。
4.2,Entity,DomainModel,DTO 之间的数据拷贝
三层或者多层架构,或者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>();
4.3 在WCF,WebService 上使用"实体类"
有很多朋友想在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 版本以上支持
5,SOD框架 的CodeFirst支持
最新版的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
其它:
一年之计在于春,2015开篇:PDF.NET SOD Ver 5.1完全开源