最近,我看到園子裏面有位朋友的一篇博客 《領域驅動設計系列(一):爲什麼要領域驅動設計? 》文章中有下面一段話,對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
其它: