引言: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完全開源