說要分享,我了個*,寫了一半放草稿箱了兩個星期都快發黴了,趁着週末寫完發出來吧。數據庫
文章分爲五部分:設計模式
文章的主要部分講述的是如何利用Entity Framework同時知足數據存儲和麪向對象應用的最優化,因此整體上能夠當作是一大波:數據庫這麼設計,而後面向對象那邊這麼用,可讓數據(更符合範式/效率更高/更方便管理),而後讓面向對象利用數據(更方便/更高效/更安全)。緩存
與許多觀點不一樣,我認爲ORM不只不必定會由於「阻抗失配」致使數據庫性能降低、潛能得不到發揮,反而以爲ORM能夠挖掘出數據庫更大的潛能,經過更合理的使用在不少地方提升其使用性能,將數據解放到業務中去。安全
如今有點晚了,我公司在山上,得趕着下山,沒來得及審稿,因此有錯誤歡迎指正。服務器
我的以爲有點趕,並且有點長,因此排列文本控制得不是很好,有待再繼續補充或者修改。架構
======================分割線,專治強迫症,下面是半個月前的內容-------------------app
上次才說要分享去年的項目,此次一會兒被新的財務系統耗了三個多月,因此就乾脆先分享下新系統中的內容。框架
總要有一個點開始展開進行分享,因此就從EF來進行展開,反正EF是貫穿頭尾的。若是順利的話或許還能夠有《EF 和 業務邏輯》、《EF 和 Web》、《EF 和 小蘋果》......數據庫設計
畢竟這篇文章不叫作《Pro EF》之類的講原理,其目的是講述在項目中對EF的應用和理解,因此涵蓋不全,多多包涵。ide
ORM好比EF的使用有幾重境界,不少來講跟框架自己的能力有關,最好的狀況下也不過能停留在挖掘下ORM的功能而已。不少我見到的ORM使用不過是僅僅爲了替代SQL,CRUD的思想並無什麼改變,全部不少項目還有Repository一層。
數據庫是CRUD的,面向對象是「方法與事件」的,何況它們自己就有「阻抗失配」,就不是一個種族的。這裏我並不想說明到底哪一個好或者應該如何,我只是想在項目收尾階段分享一下我在「假如更面向對象」的一次實踐。
此次項目是接手去年不成功的財務項目,最終決定由我重作。反正基本上一我的從頭至尾,那我就「放肆」下大膽地全OO式地進行。數據做爲貫穿項目總體的一方面,已EF爲基礎,天然佔了很重一部分,因此從EF進行擴展也是不錯的選擇。
第一部分Basic會簡單地帶過一些我對EF相關基礎的「認識」,認識EF的跳過就行了;Class部分特意交代一些面向對象的用法;Business簡單說明下在業務中的應用。
項目和示例中都採用Model First,基於EF五、VS 20十二、SQL Server 2012。示例會在實際項目和Demo中穿插引用。
最後不得不提到的是,Entity Framework只有與LinQ相伴才能如此愉快地玩耍,因此中間穿插的LinQ就很少提了。
Entity Framework Model First在開發過程當中適合敏捷的基礎是:圖形化的設計器、數據庫與模型的快速同步。
同步方面,因爲個人打算是「更面向對象」,因此同步僅使用用模型生成數據庫的方式。首先須要建立項目、在項目中添加ADO.NET Entity Data Model、在數據庫服務器中建立相應的數據庫。爲了方便區分,我使用了相同的命名:
在建立好Model後,在圖形設計界面中,右鍵而後選擇將模型同步到數據庫中,便可建立相應的SQL;運行該SQL,便可在相應數據庫中建立對應的表與關係:
在項目代碼層面,在生成Entity Model的同時,會建立一個繼承與DbContext的類。該類抽象地說,對應的是指定的數據庫,全部對於相關的數據庫的訪問都「通過」或者「經過」該類。在後來的EF版本中,這個DbContext的派生類基本上都命名爲「Entity Model名稱+Container」的形式,我習慣稱這個類的實例爲entities,由於從前的命名方式是「Entity Model名稱+Entities」:
順便簡單地說明下CRUD的簡單實現方式:
public MyFirstEntity Create() { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //建立新的實體 var newMyFirstEntity = new MyFirstEntity(); //將新對象插入到序列中 entities.MyFirstEntities.Add(newMyFirstEntity); //執行數據操做 entities.SaveChanges(); //返回最新建立的實體 return newMyFirstEntity; } public MyFirstEntity Retrieve(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到實體 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //返回查找到的實體 return myFirstEntity; } public void Update(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到實體 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //修改實體 /*此處略去修改實體代碼*/ //保存修改 entities.SaveChanges(); } public void Delete(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到實體 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //刪除實體 entities.MyFirstEntities.Remove(myFirstEntity); //保存修改 entities.SaveChanges(); }
在設計器中建立相應的Entity,就會在項目中建立相應的Class,同步到數據庫後,就會建立相應的Table。Table的名稱會是Entity的複數形式,大部分狀況下語法是沒有錯的。
對應地,在上面的CRUD示例中已經說明了如何訪問這張表的數據了。
在設計器中,能夠爲指定的Entity添加屬性,添加屬性後能夠經過Properties進行設計。同步到數據庫後,對應的屬性會生成對應的Table Column。固然,在項目中,也會在原有的類上添加相應的屬性。相關的類型的狀態會自動匹配。能夠看到用相同顏色標記的對應的對象在不一樣方面的體現,好比類型、屬性名稱、可空性(Nullable)。
在這裏,我建立了第二個實體,而後添加了與第一個實體的關聯。在添加關聯中,我選擇了一對多,而且在設計時,使用了默認生成的「引用名」,先看看結果再講解:
能夠看到,在數據庫層面,也生成了相應的一對多關係。其實現方法是經過「多」的那張表添加的一個指向「一」表主鍵的外鍵而實現的,基本還很好理解。
回到對象層面,能夠看到「一」的類MyFirstEntity中多出了一個MySecondEntites的集合屬性,要注意命名仍是自動複數的。對應的MySecondEntity裏也有一個單一的MyFirstEntity屬性。
假設myFirstEntity爲一個MyFirstEntity的實例,mySecondEntity爲一個MySecondEntity的實例。那麼,在C#中,訪問myFirstEntity中關聯的全部MySecondEntity的方式即爲:
myFirstEntity.MySecondEntities
同理,在mySecondEntity中訪問相關聯的myFirstEntity爲:
mySecondEntity.MyFirstEntity
要注意的是,在對象層面的關係中,還有「0..1」,即1個或者0個,在數據庫實現中,則是一個可空的外鍵。
注意我在添加關係的時候,並無勾選「添加外鍵屬性到...」,整個項目中都沒有。在以往的經驗裏,外鍵容易讓開發人員「像操做關係數據庫同樣操做數據」,致使代碼陷入一種很奇怪的情形。
在這裏,不得不說到面向對象和關係數據庫的「關係」問題。首先插入一個數學式子:
若是用天然語言舉例來講,那就是,若是每一個A有(關聯)一個B,每一個B有(關聯)一個C,那麼每一個A有(關聯)一個C。
在說到具體事例前,我再探討一下數據庫範式,細就不講,數據庫範式的做用就是爲了使得數據冗餘最少,這麼講應該沒什麼歧義。
那麼個人結論就來了:使用EF(或者相似ORM)能使得數據庫設計更容易實現更高範式。
舉個例子,若是每一個A對應一個B,每一個B對應一個C,...,每一個Y對應一個Z;那麼,每一個A對應一個Z。
若是我擁有一個A的實例a,我須要獲取其對應的實例z,那麼應該怎麼實現?
若是是用SQL,那麼問題就變成:「若是我有一個A的主鍵ID,如何獲取相關的Z的Row?」
多是我的並不熟悉SQL,因此在我有限的SQL知識看來,我須要些一段很長很長很長很長很長很長很長很長的SQL才能查到我要的數據。因此通常出現這種狀況,我會「*他*的範式!」而後直接拉一條A與Z的關聯,雖然不只沒遵循範式,並且連「環」都產生了。
若是在EF,這麼簡單地寫就能夠訪問到了:
a.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z;
固然,實際上仍是執行了一段可怕的SQL,可是由於用起來簡單了,因此更加不須要隨意打破範式了。
若是我常常須要從A訪問Z,須要:
a.Z;
那麼就應該參考後面的「擴展字段」部分了。
複雜一點,若是R(A, B) = 1, R(B, C) = n,那麼對應R(A, C)爲n沒錯;若是R(A, B), R(B, C) = n,那麼R(A, C) = n ^2,在C#中訪問A的C集合固然用LinQ的SelectMany了;若是從A到Z都那麼成倍又須要訪問,又不想每次寫那麼多SelectMany嵌套呢?那就只好參考後面「擴展字段」部分了。
在關係數據庫中,「關係」僅僅包含着1對n,n對n爲兩個1對n組合;而在面向對象裏,因爲對象更多地接近使用天然語言的方式描述「關係」,其「關係」就變得複雜而很差理解了。更多的內容,還須要講述完下面「繼承與關係」才能比較好展開。
另外能夠說明下對關係進行復制,也就是添加關聯,能夠簡單地進行對象複製,而不須要觸動到外鍵:
myFirstEntity.MySecondEntites.Add(newMySecondEntity);
這裏示範的是一對多的關係,而一對一的狀況下只須要直接賦值便可。
雖然做爲ORM,必然有一半關於關係數據庫,另外一半關於面向對象,而我確把關於關係數據庫的部分放到了「基礎」。由於整體來講,我以爲ORM的發展是沿着「SQL=>Data Framework(如ADO.NET)=>OO」的趨勢下去的,因此把OO部分放到了「更高級別」的地方。
另外,在這種劃分狀況下,能夠大體地理解爲我把「存儲在數據庫中的」放到了基礎,把「另外表如今面向對象中的」放到了後面。
既然是面向對象,那固然要有最基礎的關係——繼承。
在EF中,能夠設計兩個類爲基類子類關係,甚至一個基類多個子類。
一樣在設計器中添加繼承便可,能夠建立完父類子類後再「添加繼承」,或者直接在建立子類的時候選擇父類:
在設計時,咱們「建立了一個基類和繼承與該基類的子類」,而在數據庫中則是「建立了一個表以及一張關係與其1對1/0的從表」。在數據庫層面,這兩張表共用ID,固然了,由於他們是一一對應的關係,同值的主鍵和直觀;在類層面,能夠看到只有Base纔有ID,Sub在定義上沒有,實際上「繼承了」Base的ID,也擁有一個ID。
要注意的是,在DbContext處,並無產生Sub的DbSet序列屬性,須要經過Base來才能訪問獲得Sub,使用OfType<TSub>():
var subs = entities.Bases.OfType<Sub>();
插入Sub實例/行的時候,也須要經過Bases:
entites.Bases.Add(new Sub()); entities.SaveChanges();
在表裏面都不過是數據字段,而在面向對象(基本上指代C#一類傳統面嚮對象語言)中,是有「訪問性」一說。
在上面的基礎上,我能夠把基類設置爲抽象類,把字段設置訪問性(public/protected/privite/internal),右鍵相關的實體/字段,選擇屬性(Properties):
而在數據庫部分,則只是比上面多添加了一個SubProperty的Column而已。用相關訪問行的方法,能夠很好地在應用層保護數據的邏輯安全性。
抽象類保證了沒法在繞過子類(不插入表Bases_Subs數據)的狀況下建立基類對象(插入Bases數據),保證了數據上的匹配度;而字段訪問行則控制了的讀寫。
不要被小標題迷惑了,跟「C#擴展方法」沒有半毛錢關係,也不是一樣層面的事物。這裏指的是,在不增長數據庫字段的狀況下擴充類層面的字段。
實現擴展字段的基礎,是全部的模板自定義建立類都是partial的。由於每次更改模板,全部在該Data Model文件下的類,包括DbContext都會從T4模板從新生成一次,因此你沒法在原有的類基礎上進行任何修改,那麼默認類爲partial則尤其重要。
只要建立一個類,定義爲上面Base的partial類,並添加一個ExtendProperty字段,生成表後與以前並無什麼區別,自定義擴展的字段並不會生成到表上:
而後,在空數據庫的場景下運行了下面這段代碼,能夠看到當時Watch的結果(我已經把SubProperty的訪問性改回了可寫):
而後在運行一次下面的比對代碼:
能夠看到兩次結果大相徑庭,第二次的ExtendProperty字段爲空。由於第一次獲取的是一開始插入Base列表的對象,緩存了起來,而第二次是從新重數據庫中讀取的對象,數據庫中並無該字段,而一開始的緩存中有,因此產生了這項差別。
同時咱們能夠參考數據庫中的數據:
同時也能夠注意到,Bases_Sub表的主鍵是可讀寫的,而Bases表的主鍵是自增的。也就是說Bases_Sub表的主鍵是同步自Bases表對應的條目。
經過擴展字段,咱們能夠很好地讓不少重用性高的訪問方式被「優雅地」封裝起來,好比下面跟/子節點/孫節點的例子:
在這裏,我建議列表中儘可能返回LinQ表達式值,而儘可能不返回任何包含實例化行爲的序列,以進行性能保護。
不該隨意使用擴展字段做爲LinQ查詢條件,由於在數據庫中不存在的字段勢必會形成全集無索引的遍歷。
至於計算出的擴展的字段,我建議儘可能使用緩存,而不每次都進行計算。
有了上面的的基礎和擴展字段,就能夠進行一些「多態」一點的行爲。
繼續上面的例子,在基類上添加一個抽象方法,而後「迫使」子類必須實現該方法:
在這裏要注意的是Base自己就是虛類,但在partial裏不須要重複聲明,寫上也無妨。
跟上面的擴展出來的字段同樣,在表中都不會產生字段。
有了上面的建模,以後就是如何使用進行業務操做的問題了。
在一次EF的數據操做中,整體上是包含下面四部分:
最後一步在寫操做的時候纔會發生/使用。
其中要注意的是,全部操做都是圍繞同一個DbContext實例進行的。
首先看看下面代碼,firstBase和firstBase2的值如同上面所設定,顯而易見:
再往下運行纔是我想說的重點:
能夠看到,即便獲取對象在保存對象以後,存在明顯的先寫後讀的關係,不一樣DbContext(文中的entities和entites2)對象取出的數據所在歷史階段也不一致。
也就是說,一旦一個DbContext獲取過某個數據以後,就會產生數據緩存,而該緩存的做用域在於該DbContext上。
一樣的道理能夠擴充到:對同一個DbContext獲取到的對象進行修改,只能經過該DbContext進行保存修改。
特別是還要擴充到:只有來自同一個DbContext的對象才能夠進行關聯而且保存,不然在保存的時候會發生異常。
正因如此,纔會產生下面的「協做與事務」的問題,也是這一節的三個小節衍生源。
什麼是面向對象的工做場景?我看到印象最深入的解釋竟然是來自iOS開發的官方入門教程,大體的內容就是說,「你能夠認爲是一羣存在的對象互相協做並完成工做。」翻不出來了,不過大體如此。
在這裏我並不想徹底「顛覆」純倉儲模式,只是CRUD地工做讓我以爲很彆扭,當方法足夠多的時候難以對業務進行重用等等。而在使用EF的時候我也再也不寫一個包含Repository的層,由於我以爲不少餘,另外一個就是我但願儘可能地減小層次,以簡化系統複雜度。
另外摘錄DbContext的註釋的第一段話,或許比較有參考價值:
上面說到,一個DbContext實例「已是」一個工做單元和倉儲模式的混合。這也很容易理解,由於一個DbContext實例已經包含了CRUD全功能。不過這個實例如同業務的需求同樣,它是完整的,也是「殘缺」的。若是隻CRUD那麼每次業務就是:選取數據=》更新數據=》持久化。而數據只是系統運行時的業務狀態值,而不是系統自己。系統的目的是爲了實現業務而存在的,而不是持有業務的數據。
回到這裏,即時DbContext包含了CRUD,它也沒有很好地表達出業務。舉例說,用戶提交一張申請單,若是咱們將其解釋爲「用戶申請XXX」,那麼咱們但願見到代碼中表現出來的不是:
entities.Applications.Add(new Application { Title = xxx, Balance = xxx }); entities.SaveChanges();
而是:
applicationService.Apply(title, balance);
下者不過是上者的一個封裝,可是卻又不同的意義。咱們使用OO是爲了讓代碼更貼近現實,而讓創造出的系統更加符合現實業務需求。也就是隻使用DbContext只能描述對數據的操做,卻沒法描述業務,因此咱們須要一個業務層。
描述業務能夠有不少種甚至無數種建模,由於現實中人看問題有不少種角度,好比同爲申請單業務,也能夠這麼理解:
user.ApplyBalance(title, balance);
「讓用戶去申請」,看似也沒錯。甚至在建模中能夠用不少誇張的、絢麗的設計模式,但那倒是最糟糕的設計模式,由於模式太多了、沒法維護、浪費代碼等等。
我須要一個設計的方式,讓整體保持一致,同時易於理解。借鑑DDD,我把在業務中存在的對象劃分紅了兩個角色:實體與勞工。
實體指代的是數據對象,而勞工則是管理、操做、加工數據對象的工做者。「實體」大多數狀況下就是一個定義好的EF對象,畢竟我使用EF做爲主要的數據操做方式。
這與傳統的三層架構很類似,或許是異曲同工。我在勞工使用的後綴爲「Manager」,或者少數狀況爲了細化,一樣會使用工做者的命名方式,好比:Helper、Provider、Worker......重要的是,我把它們抽象成了一個顯示存在的運行機器或者團隊,有着像工程運行同樣的從屬關係、合做關係。
若是要有個貼切的簡述,那就是:勞工是有主動性的有生命的,實體則是勞工們勞做中的操做對象和產出,同時也不排除一個領域的勞工和實體同時存在,好比用戶(User)和管理用戶的勞工(UserManager)。
我相信每一個勞工的職責都應該是單一的、封閉的,因此將全部勞工都設計成職責單一封閉。而實現的依據即是一開始的需求文檔:
左邊是文檔中的結構,右邊是代碼中的結構,基本上能與文檔一一對應。實現的類的描述也是儘可能語義化,這樣作的目的是爲了貼合需求,並且減小代碼與需求間的阻隔,讓代碼更易維護。這裏咱們仍是講EF的文章,所就很少作擴展了。
粗俗地說,能夠理解爲,『匯率Manager』就只管理匯率,而『申請單Manager』就只管理申請單。
勞工們的關係也簡單地劃分爲兩種:
固然,其中的缺點也就是沒法進行循環依賴。
這裏有一個灰色地帶要劃清界限,也就是獲取關聯數據的問題。
假如你擁有A類的實體a和老公AManager,A與B類有關係,那麼應該直接經過a獲取相關的b,即 b = a.B; ,而再也不經過AManager獲取。由於設計中認爲,全部與A相關的實體,即A鎖擁有的「關係」,都應該是A的特性/屬性,是A的一部分。
後面會講到Entity的設計模式以達成相關任務。
上面或許已經發現一個矛盾了:EF全部原子化的操做都依賴於單一的DbContext實例,若是我把指責拆分並封閉起來,那麼他們就必須各自使用本身的DbContext對象,而沒法共同處理事務。
如下解決方案能夠簡單否決:
而我能想到的最優雅的解決方案就是:在須要共同執行事務的時候注入DbContext並,而且用事務進行包裹。
首先,在每一個勞工都依賴於一個DbContext的狀況下,我寫了一個公共接口與基類分別爲:
1 /// <summary> 2 /// 依賴於Entity Framework作數據存儲的方法的類的接口 3 /// 包含同步Entity Framework DbContext的方法,已實事務處理 4 /// </summary> 5 public interface IEfDbContextDependency 6 { 7 /// <summary> 8 /// 設置當前對象的Entity Framework DbContext 9 /// </summary> 10 /// <param name="dbContext">傳入的dbContext</param> 11 void SetDbContext(Finance2Container dbContext); 12 13 /// <summary> 14 /// 設置當前對象的Entity Framework DbContext 15 /// </summary> 16 /// <param name="dbContext">傳入的DbContext</param> 17 /// <param name="lastContext">當前對象原來使用的DbContext</param> 18 void SetDbContext(Finance2Container dbContext, out Finance2Container lastContext); 19 20 /// <summary> 21 /// 臨時注入DbContext,並執行Callback 22 /// 在執行過程當中會用lock鎖住本對象 23 /// 在執行完成後會被還原 24 /// </summary> 25 /// <param name="dbContext">須要注入的DbContext</param> 26 /// <param name="callback">回調</param> 27 void InjectDbContextTemporarily(Finance2Container dbContext, Action callback); 28 29 /// <summary> 30 /// 臨時注入DbContext,並執行Callback 31 /// 在執行過程當中會用lock鎖住本對象 32 /// 在執行完成後會被還原 33 /// </summary> 34 /// <typeparam name="TOut">回調後返回的類型</typeparam> 35 /// <param name="dbContext">須要注入的DbContext</param> 36 /// <param name="callback">回調</param> 37 /// <returns>回調後返回值</returns> 38 TOut InjectDbContextTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback); 39 }
1 /// <summary> 2 /// 依賴於Entity Framework作數據存儲的方法的類的基類 3 /// 包含同步Entity Framework DbContext的方法,已實事務處理 4 /// </summary> 5 public abstract class EfDbContextDependencyBase : IEfDbContextDependency 6 { 7 #region dependencies 8 9 /// <summary> 10 /// DbContext對象,以Repoisitory模式設計,因此使用倉庫名entities 11 /// 任何對entities的賦值操做都會對iEfOperatorsToSync進行同步 12 /// </summary> 13 /// <value>The entities.</value> 14 protected Finance2Container entities 15 { 16 get 17 { 18 return this._entities; 19 } 20 set 21 { 22 this._entities = value; 23 24 //同步DbContext 25 this.syncIEfDbContextDependencies(); 26 } 27 } Finance2Container _entities; 28 29 #endregion 30 31 32 #region constructors 33 34 /// <summary> 35 /// 初始化<see cref="EfDbContextDependencyBase" />類 36 /// </summary> 37 /// <param name="_entities">必須傳入一個DbContext對象</param> 38 /// <param name="efDbContextDependencies">須要自動同步DbContext的業務對象</param> 39 public EfDbContextDependencyBase(Finance2Container _entities, params IEfDbContextDependency[] efDbContextDependencies) 40 { 41 this.entities = _entities; 42 43 //設置業務對象DbContext自動同步 44 this.SetSyncIEfDbContextDependencies(efDbContextDependencies); 45 } 46 47 #endregion 48 49 50 #region private methods 51 52 /// <summary> 53 /// 須要同步DbContext的IEfDbContextDependency序列 54 /// </summary> 55 private IEfDbContextDependency[] iEfOperatorsToSync; 56 57 /// <summary> 58 /// 同步IEfOpertors中的DbContext 59 /// </summary> 60 private void syncIEfDbContextDependencies() 61 { 62 if (iEfOperatorsToSync != null) 63 { 64 foreach (var @operator in iEfOperatorsToSync) 65 { 66 @operator.SetDbContext(this.entities); 67 } 68 } 69 } 70 71 /// <summary> 72 /// 設置須要同步DbContext的IEfDbContextDependency對象,設置以後被設置的對象會在本對象被調用時 73 /// 設置以後,當本對象的entities被做任何set操做都會致使operators內容的同步 74 /// </summary> 75 /// <param name="operators">The operators.</param> 76 protected void SetSyncIEfDbContextDependencies(params IEfDbContextDependency[] operators) 77 { 78 this.iEfOperatorsToSync = operators; 79 80 //經過從新複製entities進行同步 81 this.entities = this.entities; 82 } 83 84 /// <summary> 85 /// 臨時注入DbContext,並執行Callback 86 /// 在執行過程當中會用lock鎖住全部注入對象 87 /// 在執行完成後會被還原 88 /// </summary> 89 /// <param name="dbContext">須要注入的DbContext</param> 90 /// <param name="callback">回調</param> 91 /// <param name="efDbContextDependencies">須要被注入的對象列表</param> 92 protected void InjectAllDbContextsTemporarily(Finance2Container dbContext, Action callback, params IEfDbContextDependency[] efDbContextDependencies) 93 { 94 if (efDbContextDependencies == null || efDbContextDependencies.Count() == 0) 95 { 96 callback(); 97 return; 98 } 99 //遞歸注入 100 efDbContextDependencies 101 .First() 102 .InjectDbContextTemporarily(dbContext, 103 () => 104 { 105 this 106 .InjectAllDbContextsTemporarily(dbContext, callback, efDbContextDependencies.Skip(1).ToArray()); 107 }); 108 } 109 110 /// <summary> 111 /// 臨時注入本對象的DbContext,並執行Callback 112 /// 在執行過程當中會用lock鎖住全部注入對象 113 /// 在執行完成後會被還原 114 /// </summary> 115 /// <param name="callback">回調</param> 116 /// <param name="efDbContextDependencies">須要被注入的對象列表</param> 117 protected void InjectAllDbContextsTemporarily(Action callback, params IEfDbContextDependency[] efDbContextDependencies) 118 { 119 this 120 .InjectAllDbContextsTemporarily(this.entities, callback, efDbContextDependencies); 121 } 122 123 /// <summary> 124 /// 臨時注入DbContext,並執行Callback 125 /// 在執行過程當中會用lock鎖住全部注入對象 126 /// 在執行完成後會被還原 127 /// </summary> 128 /// <typeparam name="TOut">回調後返回的類型</typeparam> 129 /// <param name="dbContext">須要注入的DbContext</param> 130 /// <param name="callback">回調</param> 131 /// <param name="efDbContextDependencies">須要被注入的對象列表</param> 132 /// <returns>回調後返回值</returns> 133 protected TOut InjectAllDbContextsTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback, params IEfDbContextDependency[] efDbContextDependencies) 134 { 135 if (efDbContextDependencies == null || efDbContextDependencies.Length == 0) 136 { 137 return callback(); 138 } 139 //遞歸注入 140 return efDbContextDependencies[0] 141 .InjectDbContextTemporarily(dbContext, 142 () => 143 { 144 return this 145 .InjectAllDbContextsTemporarily(dbContext, callback, efDbContextDependencies.Skip(1).ToArray()); 146 }); 147 } 148 149 /// <summary> 150 /// 臨時注入本對象的DbContext,並執行Callback 151 /// 在執行過程當中會用lock鎖住全部注入對象 152 /// 在執行完成後會被還原 153 /// </summary> 154 /// <typeparam name="TOut">回調後返回的類型</typeparam> 155 /// <param name="callback">回調</param> 156 /// <param name="efDbContextDependencies">須要被注入的對象列表</param> 157 /// <returns>回調後返回值</returns> 158 protected TOut InjectAllDbContextsTemporarily<TOut>(Func<TOut> callback, params IEfDbContextDependency[] efDbContextDependencies) 159 { 160 return this 161 .InjectAllDbContextsTemporarily(this.entities, callback, efDbContextDependencies); 162 } 163 164 #endregion 165 166 167 #region IEfDbContextDependency members 168 169 /// <summary> 170 /// 設置當前對象的Entity Framework DbContext 171 /// </summary> 172 /// <param name="dbContext">傳入的dbContext</param> 173 public virtual void SetDbContext(Finance2Container dbContext) 174 { 175 this.entities = dbContext; 176 } 177 178 /// <summary> 179 /// 設置當前對象的Entity Framework DbContext 180 /// </summary> 181 /// <param name="dbContext">傳入的DbContext</param> 182 /// <param name="lastContext">當前對象原來使用的DbContext</param> 183 public virtual void SetDbContext(Finance2Container dbContext, out Finance2Container lastContext) 184 { 185 lastContext = this.entities; 186 187 this.SetDbContext(dbContext); 188 } 189 190 /// <summary> 191 /// 臨時注入DbContext,並執行Callback 192 /// 在執行過程當中會用lock鎖住本對象 193 /// 在執行完成後會被還原 194 /// </summary> 195 /// <param name="dbContext">須要注入的DbContext</param> 196 /// <param name="callback">回調</param> 197 public virtual void InjectDbContextTemporarily(Finance2Container dbContext, Action callback) 198 { 199 //在強行替換DbContext時須要鎖住對象以避免出現數據亂流問題 200 lock (this) 201 { 202 //替換並暫存原有DbContext 203 Finance2Container lastFinanceDbContext; 204 this.SetDbContext(dbContext, out lastFinanceDbContext); 205 206 //回調 207 callback(); 208 209 //還原原有DbContext 210 this.SetDbContext(lastFinanceDbContext); 211 } 212 } 213 214 /// <summary> 215 /// 臨時注入DbContext,並執行Callback 216 /// 在執行過程當中會用lock鎖住本對象 217 /// 在執行完成後會被還原 218 /// </summary> 219 /// <typeparam name="TOut">回調後返回的類型</typeparam> 220 /// <param name="dbContext">須要注入的DbContext</param> 221 /// <param name="callback">回調</param> 222 /// <returns>回調後返回值</returns> 223 public virtual TOut InjectDbContextTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback) 224 { 225 //在強行替換DbContext時須要鎖住對象以避免出現數據亂流問題 226 lock (this) 227 { 228 //替換並暫存原有DbContext 229 Finance2Container lastFinanceDbContext; 230 this.SetDbContext(dbContext, out lastFinanceDbContext); 231 232 //獲取執行結果 233 var result = callback(); 234 235 //還原原有DbContext 236 this.SetDbContext(lastFinanceDbContext); 237 238 //返回執行結果 239 return result; 240 } 241 } 242 243 #endregion 244 }
裏面重要的是「Inject」一類的代碼,做用也就是將DbContext實例注入到勞工中,使得在相關事務中勞工們使用的是同一個DbContext,爲了方便,也附帶了泛型和集體注入的實現方式。暫時來講我的以爲這是個很彆扭的辦法,以前一直在尋求一種自動的依賴注入方式,但願至少能將DbContext注入到一次HttpRequest中,有實現方法的朋友能夠@下我,不勝感激。
另外一件須要提到的就是事務,是這個事務(Transaction),須要使用Transaction才能把一件原子化的事務包裝起來,以保證相關業務操做如設想同樣,好比申請單若是提交到一半發生異常,那麼應該是都不提交。
加上事務後的用法,應該是:
1 voic DoSomeWork() 2 { 3 this.InjectAllDbContextsTemporarily(() => 4 { 5 using (scope = new TransactionScope()) 6 { 7 this.doMyJob(); 8 bManager.DoOtherJobs(); 9 scope.Complete(); 10 } 11 }, bManager); 12 }
也請注意引用System.Transactions。
本例,若是BManager中也有事務會不會有影響呢?不會,由於事務是能夠嵌套的,最後執行時會是最外層的TransactionScope.Complete()方法。
這是我以爲在使用EF時最有趣的地方,由於你能夠在數據模型的基礎上根據本身的須要,爲其添加跟多的業務特性,而無需影響到數據自己。不少時候,由於這些特性,使得你能夠更加傾向於就此解決問題,而不打破範式以求全。整體來講,如下設計模式的目的就是爲了保持數據與其結構的純潔性的狀況下知足業務的遍歷性。
若是存在一個屬性或引用,它的名稱不符合業務描述,那麼能夠將它重命名。
有兩種方式,一種是在設計器裏,將其重命名,好比A.User改爲A.Friend,以符合業務指望的描述。但有時候,這樣是不足以知足需求的,那麼就使用擴展方法將其重命名:
public partial A { public User Friend { get { return this.B } } }
參考上面關係部分的說明,若是R(A, C) = 1,存在A.B和B.C,那麼咱們能夠在不建立的A.C真實數據的情形下,建立A.C做爲A.B.C的重命名:
public partial A { public C C { get { return this.B.C; } } }
或許如下命名方式會更清晰,獲取用戶全部書,假設用戶全部書都放在一個書櫃上:
public partial User { public IEnumerable<Book> MyBooks { get { return this.Bookcase.Books; } } }
在一些帶有業務特性的狀況下,能夠在重命名的基礎上添加相關業務操做:
public partial User { public string FullName { get { return String.Format("{0} {1}", this.FirstName, this.LastName); } } }
不過要注意的是,因爲一般使用LinQ操做,因此應儘可能使用緩存,好比延遲加載:
public partial User { public string FullName { get { if (_fullName == null) { _fullName = String.Format("{0} {1}", this.FirstName, this.LastName); } return _fullName; } } string _fullName; }
固然,同時還能夠擴充爲篩選計算值:
public partial User { public User BestFriend { get { return this.Friends.Max(friend => friend.Wealth + friend.GookLooking + friend.Height); } } }
在這種狀況下就不那麼建議使用延遲加載或者緩存了,若是Max是LinQ方法,或者換成是Where、Select之類的LinQ方法,那麼留有LinQ自有的延遲加載特性比較好。
在計算的基礎上,能夠將兩個同類序列合併:
public partial BankAccount { public IEnumerable<Record> AllRecords { get { return this.InputRecords.Union(this.OutputRecords); } } }
若是對於數據,訪問行須要進行限制,已保證業務對數據的讀寫安全,那麼就將其封閉。
假如class Account有一字段Password,而該字段須要進行加密方能存儲,那麼能夠將數據的字段Password的訪問行設置爲私有的,而後重寫其讀寫方法:
//此部分爲EF自動生成部分 public Account { public string Password { private get; private set; } } public partial Account { public EncryptedPassword { get { return this.Password; } } public SetPassword(string originalPassword) { this.Password = EncryptHelper.Encrypt(originalPassword); } }
在一個「基類——派生類羣」的關係集合裏,若是數據的入口爲基類的集合,而處理中須要分類,則須要經過繼承約束,即多態實現肯定其具體特性。
固然,你可使用typeOf,或者OfType<T>()來進行歸類,但前者代碼不太美觀,後者性能較低,每次都要遍歷。
這裏是其中一種示例,首先存在如下關係,並經過設計器建立了一個Enum(枚舉)。其中環境須要在.NET Framework 4.5+和EF 5+,不然枚舉也能夠直接在項目中添加而不經過EF設計器:
而後,現有基類的擴展:
public partial class BaseType { public abstract MyType Type { get; } }
而後分別擴展派生類:
public partial class AType { public override MyType Type { get { return MyType.A; } } }
public partial class AType { public override MyType Type { get { return MyType.A; } } }
那麼,在使用中,便可分類處理:
var entities = new EntityFrameworkDemoContainer(); var allTypes = entities.BaseTypes; foreach (var type in allTypes) { switch (type.Type) { case MyType.A: Console.WriteLine("It's an A."); break; case MyType.B: Console.WriteLine("It's an B."); break; } }
在實際應用中,還能夠定義更多的方法、特性,好比序列化。
當兩個沒有類型相關的實體擁有相同特性的時候,在針對某項共性的處理中,能夠將它們歸類。
這種方式一般用共有的接口來實現,用接口來描述(非約束)其共有的特性。
好比存在以下關係:
若是我須要遍歷全部引用了(依賴於)Component的對象,那麼我須要將全部引用者同質化,依次建立如下代碼:
public interface IComponentDependency { Component Component { get; set; } }
public partial class Car : IComponentDependency { }
public partial class Plane : IComponentDependency { }
public partial class EntityFrameworkDemoContainer { public IEnumerable<IComponentDependency> AllComponentDependencies { get { return ((IEnumerable<IComponentDependency>)this.Cars) .Union(this.Planes); } } }
能夠看到,經過建立帶有Component的IComponentDependency接口,而後併爲Car和Plane添加此接口,變能夠將全部Car和Plane歸類爲ComponentDependencies而不須要經過它們擁有共同的父類實現。而使用方面,則能夠:
var entities = new EntityFrameworkDemoContainer(); var allComponentDependencies = entities.AllComponentDependencies; foreach (var componentDependency in allComponentDependencies) { Console.WriteLine("I find a component."); }
當全部業務,都是由工做實體(Unit of Work),以上定義的勞工(Worker)進行操做完成的。因爲DbContext操做的原子性,不一樣的Worker間須要共同工做就須要跨越此障礙。
勞工們一般有不少類似的工做內容,因此在接口設計的時候能夠提供一些公共接口或者實現:
經過ID獲取具體實體的接口:
/// <summary> /// 用ID獲取Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的類型</typeparam> public interface IId<TEntity> { /// <summary> /// 獲取擁有ID的Entity /// </summary> /// <param name="id">ID</param> /// <returns>Entity</returns> TEntity this[int id] { get; } }
獲取全部同類實體:
/// <summary> /// 全部Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的類型</typeparam> public interface IAll<TEntity> { /// <summary> /// 獲取全部Entity /// </summary> /// <value>全部Entity</value> IEnumerable<TEntity> All { get; } }
建立、添加、移除:
/// <summary> /// 建立實體的接口 /// </summary> /// <typeparam name="TEntity">Entity的類型</typeparam> public interface ICreate<TEntity> { /// <summary> /// 建立新Entity /// </summary> /// <returns>新Entity</returns> TEntity Create(); }
/// <summary> /// 添加Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的類型</typeparam> public interface IAdd<TEntity> { /// <summary> /// 添加一個新項 /// </summary> /// <param name="newItem">須要添加的新項</param> /// <returns>添加後的項</returns> TEntity Add(TEntity newItem); }
/// <summary> /// 移除Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的類型</typeparam> public interface IRemove<TEntity> { /// <summary> /// 移除Entity的接口 /// </summary> /// <param name="id">Entity的ID</param> void Remove(int id); }
在項目中,我並無提供更新的方法,由於嚴格地分層來講,上層沒法跨越業務沾染數據層面的DbContext對象,天然不存在更改Entity的行爲。同時也爲了業務安全和規範約束,全部更改操做都應該經過業務層面進行操做。若是隻是傳參實體,業務層則沒法把握UI等上層到底對Entity進行了什麼修改,甚至有可能對Entity相關的其餘Entity進行跨越業務的修改。在通常狀況下,連IAdd也不該該有。
當一個Worker須要另外一個Worker的協做,則直接依賴並引用對象Worker,即「藉助」該Worker。
假設有一更新申請單的方法,須要當前用戶方能進行操做,則涉及了UserManager,ApplicationManager共同工做的內容,實現當以下:
public interface IApplicationManager : IEfDbContextDependency , IId<Application> { void Edit(int applicationId, string content); }
public class ApplicationManager : EfDbContextDependencyBase , IApplicationManager { IUserManager userManager; public ApplicationManager(IUserManager userManager , MyContainer entities) : base(entities) { this.userManager = userManager; } public Application this[int id] { get { return this.entities.Applications.FirstOrDefault(application => application.Id == id); } } public void Edit(int applicationId, string content) { //獲取相關申請單 var application = this[applicationId]; //獲取當前用戶 var currentUser = userManager.CurrentUser; //只有噹噹前用戶爲申請人才能夠修改 if (application.Applicant.Id == currentUser.Id) { //修改申請單 ... this.entities.SaveChanges(); } } }
當本勞工需所處理的實體產品須要依賴於其餘勞工提供的產品,那麼就須要同化兩者的DbContext。
好比須要添加申請單,那麼就須要將當前用戶做爲申請單的申請人,引用上面的代碼並省略一部分:
public partial ApplicationManager { public Application Apply(string content) { userManager.InjectDbContextTemporarily(this.entities, () => { //獲取當前用戶,做爲申請人 var currentUser = userManager.CurrentUser; var newApplication = new Application { Application = currentUser, Content = content }; this.entities.Applications.Add(newApplication); this.entities.SaveChanges(); return newApplication; }); } }
當一個業務,須要多個Worker合做完成,而且該工序具備原子性,則將它們封裝在一個事務裏。
這裏,先用項目中的一段代碼進行示範,註釋和代碼都是項目中的:
1 /// <summary> 2 /// 添加新匯率 3 /// </summary> 4 /// <param name="exchangeRateRuleId">匯率規則ID</param> 5 /// <param name="time">添加到的時間點</param> 6 /// <param name="rate">比率</param> 7 /// <param name="inverseRate">反向比率</param> 8 /// <returns>新添加的匯率</returns> 9 /// <exception cref="GM.OA.Finance2.Domain.FinancialBase.ExchangeRate.RateIsZeroException">比率或反向比率爲0時拋出此異常</exception> 10 /// <exception cref="GM.OA.Finance2.Domain.FinancialBase.ExchangeRate.ExchangeRateAlreadyExistException">當天的匯率已設置時拋出此異常</exception> 11 public DataAccess.ExchangeRate Add(int exchangeRateRuleId, DateTime time, decimal rate, decimal inverseRate) 12 { 13 //比率不得爲0 14 if (rate == 0 || inverseRate == 0) 15 { 16 throw new RateIsZeroException(); 17 } 18 19 //若是同時間點匯率已存在,則拋出異常 20 var existExchangeRate = entities 21 .ExchangeRates 22 .FirstOrDefault(item => 23 item.ExchangeRateRule.Id == exchangeRateRuleId 24 && item.Time == time); 25 if (existExchangeRate != null) 26 { 27 throw new ExchangeRateAlreadyExistException(existExchangeRate); 28 } 29 30 //獲取匯率規則 31 var exchangeRateRule = this.ExchangeRateRuleManager[exchangeRateRuleId]; 32 33 //建立一對正反的兌換匯率 34 //正兌換匯率 35 var exchangeRate = new DataAccess.ExchangeRate 36 { 37 Time = time, 38 ExchangeRateRule = exchangeRateRule//關聯匯率規則 39 }; 40 //反兌換匯率 41 var inverseExchangeRate = new DataAccess.ExchangeRate 42 { 43 Time = time, 44 ExchangeRateRule = exchangeRateRule.InverseExchangeRateRule//關聯反向的匯率規則 45 }; 46 exchangeRate.InverseExchangeRate = inverseExchangeRate;//互相設置爲反向匯率 47 //設置匯率 48 exchangeRate.SetRate(rate, inverseRate); 49 50 //因爲匯率互爲反向匯率,致使數據對象環狀,因此須要規範順序進行事務插入 51 using (var scope = new TransactionScope()) 52 { 53 //持久化數據 54 this.entities.ExchangeRates.Add(exchangeRate); 55 this.entities.SaveChanges(); 56 57 //設置反向匯率 58 inverseExchangeRate.InverseExchangeRate = exchangeRate; 59 this.entities.SaveChanges(); 60 61 scope.Complete(); 62 } 63 64 //返回插入後的新對象 65 return exchangeRate; 66 }
這是插入一個新的匯率,好比¥兌換$的匯率,同時也要建立$兌換¥的匯率,因爲有計算偏差和一些買賣差價,因此兩個匯率不必定嚴格按照反比存在,而是手動設定的值,這是業務部分。而在實現部分,能夠知道這樣的狀況下,一個匯率會關聯兩個匯率規則(¥<=>$、$<=>¥),那麼就造成了「環」,插入的時候沒法單步執行,因此須要逐步執行(屢次SaveChanges())並經過Scope進行封裝。我想,在事務方面沒什麼疑問。
那麼,在上文裏,說到過,事務是能夠嵌套的,而嵌套以最外層的TransactionScope.Complete()爲最終執行點。當你須要多個不一樣的Work共同進行操做,而且該操做具備原子性的時候,你能夠在注入DbContext後用TransactionScope將其包裹:
1 /// <summary> 2 /// 從指定帳戶轉入指定金額到另外一個指定帳戶 3 /// 兌換四捨五入保留兩位小數 4 /// </summary> 5 /// <param name="withdrawalAccountId">轉出帳戶ID</param> 6 /// <param name="depositAccountId">轉入帳戶ID</param> 7 /// <param name="transferAmount">轉帳金額</param> 8 /// <param name="withdrawalCurrencyCode">幣別英文單位,爲空則使用默認幣別,通常認爲是人民幣</param> 9 /// <param name="exchangeRateId">匯率ID,若是不爲空則使用該匯率進行轉帳</param> 10 /// <param name="remarks">備註</param> 11 /// <returns>轉帳日誌</returns> 12 public TBalanceTransferRecord TransferTo(int withdrawalAccountId, int depositAccountId, 13 decimal transferAmount, 14 string depositCurrencyCode = null, 15 int? exchangeRateId = null, 16 string remarks = null) 17 { 18 //若是幣別爲空則設置默認幣別 19 if (depositCurrencyCode == null) 20 { 21 depositCurrencyCode = Configurations.DefaultCurrencyCode; 22 } 23 24 //處理匯率和轉入轉出具體值 25 DataAccess.ExchangeRate exchangeRate; 26 decimal withdrawalAmount; 27 decimal depositAmount = transferAmount; 28 29 //根據不一樣的匯率狀況計算轉入轉出金額 30 if (exchangeRateId == null)//沒有匯率的狀況 31 { 32 exchangeRate = null; 33 //沒有匯率的狀況下轉入和轉出對等 34 withdrawalAmount = depositAmount; 35 } 36 else 37 { 38 //獲取匯率 39 exchangeRate = exchangeRateManager[exchangeRateId.Value]; 40 41 //若是匯率爲空,則拋出異常 42 if (exchangeRate == null) 43 { 44 throw new ExchangeRateNullException(exchangeRateId.Value); 45 } 46 47 //若是匯率的外幣不是轉入幣別,則使用反向匯率 48 if (exchangeRate.ExchangeRateRule.ForeignCurrency.Code != depositCurrencyCode) 49 { 50 exchangeRate = exchangeRate.InverseExchangeRate; 51 } 52 53 //若是匯率外幣依舊不是轉入幣別,則沒法使用該匯率,拋出異常 54 if (exchangeRate.ExchangeRateRule.ForeignCurrency.Code != depositCurrencyCode) 55 { 56 throw new ExchangeRateNotMatchException(currencyManager[depositCurrencyCode], exchangeRate); 57 } 58 59 //轉出金額經過匯率換算,四捨五入保留兩位小數 60 withdrawalAmount = Math.Round(depositAmount * exchangeRate.InverseExchangeRate.Rate, 2, MidpointRounding.AwayFromZero); 61 } 62 63 //須要使用如下匯率業務,注入DbContext 64 var record = exchangeRateManager 65 .InjectDbContextTemporarily(this.entities, () => 66 { 67 //將轉入轉出集中在一個事務中 68 using (var scope = new TransactionScope()) 69 { 70 var withdrawalRecord = this 71 .AdjustBalance(withdrawalAccountId, 72 -withdrawalAmount,//!Attension!: 注意這裏爲負數,由於爲轉出 73 exchangeRate == null 74 ? depositCurrencyCode 75 : exchangeRate.ExchangeRateRule.DomesticCurrency.Code); 76 var depositRecord = this 77 .AdjustBalance(depositAccountId, 78 depositAmount, 79 depositCurrencyCode); 80 81 //建立新日誌 82 var newTransferRecord = createNewTransferRecord(withdrawalRecord, depositRecord, transferAmount, exchangeRate); 83 84 //若是類型不匹配,則拋出異常 85 if (newTransferRecord.GetType() != typeof(TBalanceTransferRecord)) 86 { 87 throw new BalanceTransferRecordTypeNotMatchException<TBalanceTransferRecord>(); 88 } 89 90 //設置日誌內容 91 var transferRecord = (TBalanceTransferRecord)newTransferRecord; 92 transferRecord.WithdrawalBalanceChangeRecord = withdrawalRecord; 93 transferRecord.DepositBalanceChangeRecord = depositRecord; 94 transferRecord.ExchangeRate = exchangeRate; 95 transferRecord.Remarks = remarks; 96 97 //持久化日誌 98 this.entities.BalanceTransferRecords.Add(transferRecord); 99 this.entities.SaveChanges(); 100 101 //結束事務 102 scope.Complete(); 103 104 return transferRecord; 105 } 106 }); 107 108 return record; 109 }
這也是項目中的代碼,即從一個帳戶用某個匯率轉帳必定金額到另外一個帳戶。
每次修改Entity Model後,直接運行生成的SQL,就能夠得到新的數據庫了,但同時也清空了原來的數據。
因此我須要一個初始化開發測試數據的方式,便在項目中添加了初始化方法,建立了一個簡單(lou)的初始化工具,方便初始化一些數據:
有了初始化工具後,項目的迭代方式大體是:修改Entity Model =》 從新生成數據庫 =》 修改/不修改初始化工具 =》 初始化數據。哪怕是一個字段的修改,均可以立刻更新數據庫設計,而後繼續下一步開發。
固然,這個流程卻不適用在生產機上,這是如今遇到的一個難題。若是發佈生產後,數據庫中有了真實數據,那麼更改就要涉及數據遷移的事了,事情就變得不那麼簡單。