實(shí)體類創(chuàng)建后在方法中對哪些屬性賦值了,傳遞到底層方法時(shí)在底層如何得知哪些屬性被賦值過。如何監(jiān)控屬性的更改,請看腦洞大開之《大花貓動了哪些小玩具》——記屬性監(jiān)控之曲線救國。
在使用EF更新數(shù)據(jù)庫實(shí)體時(shí)。很多時(shí)候我們想要的只是更新表中的某一個(gè)或部分字段。雖然可以通過設(shè)置來告訴上下文我們要更新的字段。但是一般我們都會把數(shù)據(jù)持久層封裝起來。通過泛型操作。而這時(shí)我們就無法得知應(yīng)用層面修改了哪些字段了。
最近也在學(xué)習(xí)EF,就正好遇到了這個(gè)問題。當(dāng)然,如果直接在應(yīng)用層面使用,通過設(shè)置字段的IsModified狀態(tài)就可以了。如下
db.Entry(model).Property(x => x.Token).IsModified = false;
可是,這僅限于學(xué)習(xí)和demo。正式開發(fā)中一般是不會把這種底層操作公開給應(yīng)用層面的。都會把數(shù)據(jù)庫持久層進(jìn)行封裝。然后通過實(shí)體工廠(倉庫)加實(shí)體泛型的方式提供增刪改查。
具體的可以參考《基于Entity Framework的Repository模式設(shè)計(jì)》之類的文章。
這類方式都有一個(gè)共同點(diǎn),更新和刪除的時(shí)候都有如下類似代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public virtual void Update(TEntity TObject) { try { var entry = Context.Entry(TObject); Context.Set<TEntity>().Attach(TObject); entry.State = EntityState.Modified; } catch (OptimisticConcurrencyException ex) { throw ex; } } |
個(gè)人理解:Update(TEntity TObject)通過傳遞一個(gè)實(shí)體到方法,然后附加到數(shù)據(jù)庫上下文,并將數(shù)據(jù)標(biāo)記為修改狀態(tài)。然后進(jìn)行的更新。
這種情況會對實(shí)體的所有字段進(jìn)行更新。那么我們則需要保證這個(gè)實(shí)體是從數(shù)據(jù)庫查出來的,或者與數(shù)據(jù)庫的記錄是對應(yīng)的上的。這在C/S結(jié)構(gòu)中是沒有問題的,可問題是在B/S結(jié)構(gòu)中呢?我們不可能把實(shí)體所有的字段都打包,發(fā)送到客戶端,然后客戶端修改在返回到服務(wù)端,然后在調(diào)用倉庫方法更新吧。說個(gè)最簡單的,修改用戶密碼,我們只需要一個(gè)用戶ID,一個(gè)新密碼就可以了?;蛘哝i定用戶賬號,只需要一個(gè)用戶ID,一個(gè)鎖定狀態(tài),一個(gè)鎖定時(shí)間。這樣,我們不可能把整個(gè)用戶實(shí)體打包傳來傳去吧。有人說可以在保存的時(shí)候先根據(jù)ID查一遍數(shù)據(jù)庫,然后再將修改的屬性值附加上去后再更新就可以了。這就回到問題上了:在倉庫方法中只有泛型類型,而你在調(diào)用倉庫更新方法時(shí)傳遞的是一個(gè)實(shí)體類型。倉庫并不知道你是那個(gè)實(shí)體,并且更新了哪些字段。
當(dāng)然,通過觸發(fā)器我們知道數(shù)據(jù)庫的更新都是先刪后插,所以更新幾個(gè)字段與全列更新底層操作是沒有多少區(qū)別的。
現(xiàn)在拋開倉庫更新等實(shí)體泛型等信息。就單看一下當(dāng)一個(gè)實(shí)體發(fā)生改變時(shí),我們怎么能知道他修改了哪些屬性。
正常情況下一個(gè)實(shí)體長這樣
View Code
當(dāng)我們要修改這個(gè)實(shí)體的屬性時(shí):
1 2 3 | var entity = new accountEntity(); entity.Id=1; entity.Account = "給屬性賦值'; |
然后將這個(gè)實(shí)體傳遞到底層進(jìn)行操作。
1 | db.Update(entity); |
完全沒有問題,可是我的問題在底層怎么知道我應(yīng)用層修改了那幾個(gè)屬性呢?再加一個(gè)方法,告訴底層,我修改了這幾個(gè)屬性。
1 | db.Update(entity, "Account" ); |
好像也沒有什么不可哈。
可是這樣,如果我修改了Account,參數(shù)中卻傳遞了Password怎么辦?所以,應(yīng)該在實(shí)體上就應(yīng)該有一個(gè)集合對整個(gè)屬性是否有修改的狀態(tài)進(jìn)行存儲。然后到底層Update方法在取出更新過的字段進(jìn)行下一步操作。
通過這一思路,我想到在實(shí)體中加一個(gè)字典:
1 | protected Dictionary< string , dynamic> FieldTracking = new Dictionary< string , dynamic>(); |
當(dāng)屬性賦值時(shí),則添加到字典中來。(當(dāng)然,這種操作是會增加程序的開銷的)
1 | FieldTracking[ "Account" ]= "給屬性賦值" ; |
然后在底層在取出里面的集合,來區(qū)分哪些字段被修改(大花貓動了哪些小玩具)。
改造下實(shí)體屬性
1 2 3 4 5 6 7 8 9 | public virtual string Account { get { return _Account; } set { _Account = value; FieldTracking[ "Account" ] = value; } } |
看過編譯后的IL代碼的都知道,class中的屬性最終會編譯成兩個(gè)方法 setvalue和getvalue,那么通過修改set方法添加FieldTracking["Account"] = value;就可以讓屬性在賦值的時(shí)候添加到字典中。
很簡單吧。
你以為這樣就完了。如果拿房間來比喻實(shí)體、拿玩具來比作屬性。我家那大花貓就是修改實(shí)體屬性的方法。你知道我家有多少玩具嗎?你每天回家的時(shí)候你知道大花貓動了哪個(gè)小玩具嗎?給每個(gè)玩具裝個(gè)GPS?哈哈哈哈,別鬧,花這心思還不如再買點(diǎn)回來。什么?買回來的還得裝,算了。研究下怎么裝吧。
一個(gè)程序可能有上百個(gè)實(shí)體類,修改現(xiàn)有的實(shí)體類,給每個(gè)set加一行?作為一個(gè)程序員是不可能容忍做這樣的操作的。寫一個(gè)工具,讀取所有的實(shí)體代碼,加上這一行,保存。這是個(gè)好辦法。那每次添加一個(gè)實(shí)體類就得調(diào)用工具重寫來一遍,每次修改屬性再調(diào)用一遍,恩。沒問題。能用就行。這不是一個(gè)真心養(yǎng)貓的人的人能容忍的。
那怎么辦?把貓打死?那玩具的存在將會沒有任何意義。想到一個(gè)辦法,在我離開房子的時(shí)候(程序初始化),給房子里的所有房間(實(shí)體類)創(chuàng)建一個(gè)同樣的房間(繼承),包含了與原房間所有需要監(jiān)控(標(biāo)記為virtual)的玩具的復(fù)制,在復(fù)制過程中加上GPS(-_~)。然后給貓玩。貓通過我給的門進(jìn)到這個(gè)繼承的房間中玩所有玩具的時(shí)候,GPS就能將貓的動作全部記錄下來。我一回家,這貓玩了哪些玩具一看GPS記錄就全知道了。喲,這小崽子,在王元鵝呢。
看不懂,沒關(guān)系,上馬:
1、在程序集初始化的時(shí)候,通過反射,查找所有繼承自BaseEntity的實(shí)體類。遍歷其中的屬性。找到標(biāo)記為virtual進(jìn)行復(fù)制。
剛開始對于如果找到virtual屬性花了不少時(shí)間。我總只想著在屬性上找,卻沒想到去set_value方法上去找(其實(shí)get_value方法也是)。還是太菜啊。
注:NoMapAttribute特性是一個(gè)自定義的標(biāo)記,表示不參與映射。因?yàn)椴粎⑴c映射就不需要監(jiān)控。與本文章代碼沒有太大的關(guān)系。僅供參考。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //獲取實(shí)體所在的程序集(ClassLibraryDemo) var assemblyArray = AppDomain.CurrentDomain.GetAssemblies() .Where(w => w.GetName().Name == "ClassLibraryDemo" ) .ToList(); //實(shí)體的基類 var baseEntityType = typeof (BaseEntity); //循環(huán)程序集 foreach (Assembly item in assemblyArray) { //找到這個(gè)程序集中繼承自基類的實(shí)體 var types = item.GetTypes().Where(t => t.IsAbstract == false && baseEntityType.IsAssignableFrom(t) && t != baseEntityType); foreach (Type btItem in types){ //遍歷這個(gè)實(shí)體類中的屬性 var properties = btItem.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(w => w.CanRead && w.CanWrite && w.GetCustomAttributes( typeof (NoMapAttribute), false ).Any() == false //TODO:要不要檢查get方法? && w.GetSetMethod().IsVirtual); } } |
2、根據(jù)1的結(jié)果,復(fù)制一個(gè)新的房間(動態(tài)代碼生成一個(gè)類,這個(gè)類繼承1中的實(shí)體,并且重寫了屬性的set方法)
這個(gè)過程就設(shè)計(jì)到動態(tài)代碼的生成了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //首先創(chuàng)建一個(gè)與實(shí)體類對應(yīng)的動態(tài)類 CodeTypeDeclaration ct = new CodeTypeDeclaration(btItem.Name + "_Dynamic" ); //循環(huán)實(shí)體中的所有標(biāo)記為virtual的屬性 foreach (PropertyInfo fiItem in properties) { //創(chuàng)建一個(gè)屬性 var p = new CodeMemberProperty(); //設(shè)置屬性為公共、重寫 p.Attributes = MemberAttributes.Public | MemberAttributes.Override; //override //設(shè)置屬性的類型為繼承的屬性的數(shù)據(jù)類型 p.Type = new CodeTypeReference(fiItem.PropertyType); //屬性名稱與繼承的一致 p.Name = fiItem.Name; //包含set代碼 p.HasSet = true ; //包含get代碼 p.HasGet = true ; //設(shè)置get代碼 //return base.Account p.GetStatements.Add( new CodeMethodReturnStatement( new CodeFieldReferenceExpression( new CodeBaseReferenceExpression(), fiItem.Name))); //設(shè)置set代碼 //base.Account=value; p.SetStatements.Add( new CodeAssignStatement( new CodeFieldReferenceExpression( new CodeBaseReferenceExpression(), fiItem.Name), new CodePropertySetValueReferenceExpression())); //FieldTracking["Account"]=value; p.SetStatements.Add( new CodeSnippetExpression( "FieldTracking[\"" + fiItem.Name + "\"] = value" )); //將屬性添加到類中 ct.Members.Add(p); } |
3、將剛才生成的類加到原類所在的命名空間+".Dynamic"(加后綴以示區(qū)分)
1 2 3 | //聲明一個(gè)命名空間(與當(dāng)前實(shí)體類同名+后綴) CodeNamespace ns = new CodeNamespace(btItem.Namespace + ".Dynamic" ); ns.Types.Add(ct); |
4、編輯生成代碼所在的程序集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //要動態(tài)生成代碼的程序集 CodeCompileUnit program = new CodeCompileUnit(); //添加引用 program.ReferencedAssemblies.Add( "mscorlib.dll" ); program.ReferencedAssemblies.Add( "System.dll" ); program.ReferencedAssemblies.Add( "System.Core.dll" ); //定義代碼工廠 CSharpCodeProvider provider = new CSharpCodeProvider(); //編譯程序集 var cr = provider.CompileAssemblyFromDom( new System.CodeDom.Compiler.CompilerParameters(); //看編譯是否通過 var error = cr.Errors; if (error.HasErrors) { Console.WriteLine( "錯(cuò)誤列表:" ); //編譯不通過 foreach (dynamic item in error) { Console.WriteLine( "ErrorNumber:{0};Line:{1};ErrorText{2}" , item.ErrorNumber, item.Line, item.ErrorText); } return ; } else { Console.WriteLine( "編譯成功。" ); } |
查看生成的代碼
1 2 3 4 5 6 7 8 9 10 11 12 | //查看生成的代碼 var codeText = new StringBuilder(); using ( var codeWriter = new StringWriter(codeText)) { CodeDomProvider.CreateProvider( "CSharp" ).GenerateCodeFromNamespace(ns, codeWriter, new CodeGeneratorOptions() { BlankLinesBetweenMembers = true }); } Console.WriteLine(codeText); |
5、將復(fù)制的新類與原類建立映射關(guān)系。
1 2 3 4 5 | foreach (Type item in ts) { //注冊(模擬實(shí)現(xiàn),通過字典實(shí)現(xiàn)的,也可以通過IOC注入方式處理) Mapping.Map(item.BaseType, item); } |
6、獲得這個(gè)復(fù)制的實(shí)體對象
1 2 | //創(chuàng)建一個(gè)指定的實(shí)體對象 AccountEntity ae = Mapping.GetMap<AccountEntity>(); |
7、對這個(gè)實(shí)體對象的屬性進(jìn)行賦值
1 2 3 4 5 6 7 | //主鍵賦值不會修改屬性更新 ae.BaseEntity_Id = 1; //不會變(未標(biāo)記為virtual) ae.MainEntity_Name = "大花貓" ; ae.MainEntity_UpdateTime = DateTime.Now; //修改某個(gè)屬性 ae.Account = "admin" ; ae.Account = "以最后一次的修改為準(zhǔn)" ; |
8、調(diào)用底層方法,底層根據(jù)這個(gè)實(shí)體屬性獲得被修改的屬性名稱
1 2 3 4 5 6 7 | //調(diào)用基類中的方法 獲取變動的屬性 var up = ae.GetFieldTracking(); Console.WriteLine( "有修改的字段:" ); up.ForEach(fe => { Console.WriteLine(fe + ":" + ae[fe]); }); |
9、完美
就這樣,在底層就能知道哪些實(shí)體被賦值過了。
當(dāng)然,有些實(shí)體我們只是需要用來計(jì)算,則可以調(diào)用方法將賦值過的屬性進(jìn)行刪除
1 2 | //刪除變更字段 ae.RemoveChanges( "Account" ); |
這只是一個(gè)簡單的實(shí)現(xiàn),還有一種比較復(fù)雜的情況,在第6步,獲得這個(gè)復(fù)制的實(shí)體對象時(shí),怎么用一個(gè)現(xiàn)有的new出來的實(shí)體對象去創(chuàng)建建并監(jiān)控呢。就像,別人送我一房間現(xiàn)成的玩具,給我的時(shí)候貓就在里面玩了。嗷,把貓打死吧。
總結(jié):
再次認(rèn)識到反射的強(qiáng)大。
也第一次實(shí)現(xiàn)了代碼生成代碼并使用的經(jīng)歷。
對字段和屬性的區(qū)別有了更深的認(rèn)識。
對訪問修飾符和虛virtual方法有了更好的認(rèn)識。
本文僅供參考,如果你能通過閱讀本文解決你的問題或能學(xué)到點(diǎn)什么那就更好了。
http://www.cnblogs.com/longtushen/p/7225288.html