SOD框架的数据容器,打造最适合DDD的ORM框架

时间:2022-12-23 09:25:14

SOD框架的数据容器,打造最适合DDD的ORM框架

引言: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,准备工作

比如有下面一个用户信息类接口:

SOD框架的数据容器,打造最适合DDD的ORM框架
 public interface IUser
{
int Age { get; set; }
string FirstName { get; set; }
string LasttName { get; set; }
int UserID { get; set; }
}
SOD框架的数据容器,打造最适合DDD的ORM框架

然后根据这个接口,写一个PDF.NET SOD 实体类 UserEntity ,用于持久化数据到数据库或者其它用途:

还有一个用户类的DTO类 UserDto,可用于分布式系统的数据传输或者解决方案多个项目分层之间的数据传输:

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个数据:

SOD框架的数据容器,打造最适合DDD的ORM框架
            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);
SOD框架的数据容器,打造最适合DDD的ORM框架

程序输出:

user["Age"] == null :True
user.Age:0

2.2.2,数据库中的 NULL

为了验证SOD 实体类从数据库查询出来的字段的空值是什么情况,我们先插入几条测试数据:

SOD框架的数据容器,打造最适合DDD的ORM框架
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 });
SOD框架的数据容器,打造最适合DDD的ORM框架

我们插入的第一条数据并没有年龄Age 的数据,下面再来查询这条数据,看数据库的值是否为NULL:

SOD框架的数据容器,打造最适合DDD的ORM框架
            //查找姓张的一个用户
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);
SOD框架的数据容器,打造最适合DDD的ORM框架

注意,这里我们在OQL的Select 子句中,指定了要查询实体类的 Age 属性,如果数据库没有该属性字段的值,它一定是NULL,也就是 程序中说的 NBNULL.Value,看输出结果验证:

user["Age"] == DBNULL.Value :True
user.Age:0

当然,这里数据库为空,要求表字段是支持可空的。

从这里我们可以得出结论:

结论二: SOD 用OQL 查询的实体类属性,如果数据库对应的字段值为空,那么实体类内部该属性值也为空(DBNull.Value)

2.3,可空类型的问题

在EF等ORM中,要定义一个字段可空,需要定义成可空类型,比如我们的User类,假设定义成EF的实体类,应该是这样子的:

SOD框架的数据容器,打造最适合DDD的ORM框架
    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; } //主键,不可为空
}
SOD框架的数据容器,打造最适合DDD的ORM框架

这种可空类型的实体类定义,能够让数据库字段标记为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实体类是允许的。
另外,这个值的可变性,使得SOD框架处理 枚举属性 非常方便,因为,Enum 与int 类型是兼容的,可以相互转换,参看这篇文章:

《 实体类的枚举属性--原来支持枚举类型这么简单,没有EF5.0也可以

属性值的可变性,除了上面的好处,还有什么好处?

好处大大的,这意味着 PropertyNames,PropertyValues 的长度是可变的,就像前面的例子,查询了Age属性,实体类的值有3个,而不查询,那么值只有2个。

假设实体类有50个属性,本次只查询了2个属性,那么SOD的实体类实际传输的数据就只有2个,而不是50个,这将大大节省数据传输量。

这个可以通过SOD实体类的序列化结果来验证。

4,在分布式系统上使用实体类

4.1,实体类的序列化与反序列化

这里必然绕不开实体类的序列化与反序列化,现在最新的SOD框架已经内置支持,参考下面的代码:

SOD框架的数据容器,打造最适合DDD的ORM框架
 //查找姓张的一个用户
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("成功");
SOD框架的数据容器,打造最适合DDD的ORM框架

下面是序列化结果的输出:

SOD框架的数据容器,打造最适合DDD的ORM框架
<?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>
SOD框架的数据容器,打造最适合DDD的ORM框架

可见,以这种方式序列化传输的数据量,将是很少的。当然,你还可以更改成JSOn序列化,这样数据更少,缺点是数据元数据没有了。

4.2,Entity,DomainModel,DTO 之间的数据拷贝

三层或者多层架构,或者DDD架构,少不了Entity,DomainModel,DTO 之间的数据拷贝,如果数据结构高度相似,可以使用AutoMapper之类的工具,而在SOD框架内,使用了速度最快的属性拷贝方案,参见之前我写的博客文章:

使用反射+缓存+委托,实现一个不同对象之间同名同类型属性值的快速拷贝

另外,如果是从实体类到DTO,或者DTO到实体类的数据复制,在EntityBase上提供了 MapFromMapTo方法,例如下面使用的例子:

IUser TestMapFromDTO(IUser data)
{
IUser user = EntityBuilder.CreateEntity<IUser>();
((entityBase)user).MapFrom(data);
return user;
}

当然,还有CopyTo方法,只要你引用了框架扩展 PWMIS.Core.Extension.dll

SOD框架的数据容器,打造最适合DDD的ORM框架
using PWMIS.Core.Extensions;
... ... //CoyTo 创建一个实例对象
ImplCarInfo icResult= info.CopyTo<ImplCarInfo>(null); //CopyTo 填充一个实例对象
ImplCarInfo icResult2 = new ImplCarInfo();
info.CopyTo<ImplCarInfo>(icResult2);
SOD框架的数据容器,打造最适合DDD的ORM框架

将实体类的数据拷贝到DTO对象的时候,推荐下面这种直接调用 这种方式:

DTOXXX dto=EntityObject.CopyTo<DTOXX>(); 

5,SOD框架 的CodeFirst支持

最新版的SOD框架(PDF.NET SOD)已经可以方便的支持CodeFirst开发了,使用很简单,调用只需要一行代码:

 Console.WriteLine("第一次运行,将检查并创建数据表");
LocalDbContext context = new LocalDbContext();//自动创建表

而这个LocalDbContext 的定义也不复杂:

SOD框架的数据容器,打造最适合DDD的ORM框架
public class LocalDbContext : SqlServerDbContext

        public LocalDbContext()
: base("local")
{
//local 是连接字符串名字
} #region 父类抽象方法的实现 protected override bool CheckAllTableExists()
{
//创建用户表
CheckTableExists<UserEntity>();
return true;
} #endregion
}
SOD框架的数据容器,打造最适合DDD的ORM框架

综合结论:

所以SOD实体类对用户而言是透明的,它并没有增加使用的复杂性,又可以很好的控制数据量,还可以让你知道数据来自哪里,简单而又强大。

SOD框架的数据容器,打造最适合DDD的ORM框架

这样的ORM,才是合适DDD的ORM,当然,SOD不仅仅是一个ORM,它还有SQL-MAP和DataControl,具体可以看框架官网http://www.pwmis.com/sqlmap ,9年历史铸就的成果,坚固可靠。

附注:

下面是本文说明中使用的完整代码:

图片的效果要好些:

SOD框架的数据容器,打造最适合DDD的ORM框架

有关该测试程序的完整下载和查看,请看框架开源项目地址:

http://pwmis.codeplex.com/SourceControl/latest#SOD/Test/EntityTest-2013/Program.cs

其它:

一年之计在于春,2015开篇:PDF.NET SOD Ver 5.1完全开源

分类: PDF.NET
标签: ORMPDF.NETDDD