C#監控類屬性的更改(大花貓動了哪些小玩具)
在使用EF更新資料庫實體時。很多時候我們想要的只是更新表中的某一個或部分欄位。雖然可以通過設定來告訴上下文我們要更新的欄位。但是一般我們都會把資料持久層封裝起來。通過泛型操作。而這時我們就無法得知應用程式層面修改了哪些欄位了。
最近也在學習EF,就正好遇到了這個問題。當然,如果直接在應用程式層面使用,通過設定欄位的IsModified狀態就可以了。如下
db.Entry(model).Property(x => x.Token).IsModified = false;
可是,這僅限於學習和demo。正式開發中一般是不會把這種底層操作公開給應用程式層面的。都會把資料庫持久層進行封裝。然後通過實體工廠(倉庫)加實體泛型的方式提供增刪改查。
具體的可以參考《基於Entity Framework的Repository模式設計》之類的文章。
這類方式都有一個共同點,更新和刪除的時候都有如下類似代碼:
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; } }
個人理解:Update(TEntity TObject)通過傳遞一個實體到方法,然後附加到資料庫上下文,並將資料標記為修改狀態。然後進行的更新。
這種情況會對實體的所有欄位進行更新。那麼我們則需要保證這個實體是從資料庫查出來的,或者與資料庫的記錄是對應的上的。這在C/S結構中是沒有問題的,可問題是在B/S結構中呢?我們不可能把實體所有的欄位都打包,發送到用戶端,然後用戶端修改在返回到服務端,然後在調用倉庫方法更新吧。說個最簡單的,修改使用者密碼,我們只需要一個使用者ID,一個新密碼就可以了。或者鎖定使用者帳號,只需要一個使用者ID,一個鎖定狀態,一個鎖定時間。這樣,我們不可能把整個使用者實體打包傳來傳去吧。有人說可以在儲存的時候先根據ID查一遍資料庫,然後再將修改的屬性值附加上去後再更新就可以了。這就回到問題上了:在倉庫方法中只有泛型型別,而你在調用倉庫更新方法時傳遞的是一個實體類型。倉庫並不知道你是那個實體,並且更新了哪些欄位。
當然,通過觸發器我們知道資料庫的更新都是先刪後插,所以更新幾個欄位與全列更新底層操作是沒有多少區別的。
現在拋開倉庫更新等實體泛型等資訊。就單看一下當一個實體發生改變時,我們怎麼能知道他修改了哪些屬性。
正常情況下一個實體長這樣
1 /// <summary> 2 /// 一個具體的實體 3 /// </summary> 4 public class AccountEntity : MainEntity 5 { 6 /// <summary> 7 /// 文本類型 8 /// </summary> 9 public virtual string Account { get; set; } 10 /// <summary> 11 /// 又一個文字屬性 12 /// </summary> 13 public virtual string Password { get; set; } 14 /// <summary> 15 /// 數字類型 16 /// </summary> 17 public virtual int Sex { get; set; } 18 /// <summary> 19 /// 事件類型 20 /// </summary> 21 public virtual DateTime Birthday { get; set; } 22 /// <summary> 23 /// 雙精確度浮點數 24 /// </summary> 25 public virtual double Height { get; set; } 26 /// <summary> 27 /// 十進位數 28 /// </summary> 29 public virtual decimal Monery { get; set; } 30 /// <summary> 31 /// 二進位 32 /// </summary> 33 public virtual byte[] PublicKey { get; set; } 34 /// <summary> 35 /// Guid類型 36 /// </summary> 37 public virtual Guid AreaId { get; set; } 38 }
View Code
當我們要修改這個實體的屬性時:
var entity = new accountEntity();entity.Id=1;entity.Account = "給屬性賦值';
然後將這個實體傳遞到底層進行操作。
db.Update(entity);
完全沒有問題,可是我的問題在底層怎麼知道我應用程式層修改了那幾個屬性呢?再加一個方法,告訴底層,我修改了這幾個屬性。
db.Update(entity,"Account");
好像也沒有什麼不可哈。
可是這樣,如果我修改了Account,參數中卻傳遞了Password怎麼辦?所以,應該在實體上就應該有一個集合對整個屬性是否有修改的狀態進行儲存。然後到底層Update方法在取出更新過的欄位進行下一步操作。
通過這一思路,我想到在實體中加一個字典:
protected Dictionary<string, dynamic> FieldTracking = new Dictionary<string, dynamic>();
當屬性賦值時,則添加到字典中來。(當然,這種操作是會增加程式的開銷的)
FieldTracking["Account"]="給屬性賦值";
然後在底層在取出裡面的集合,來區分哪些欄位被修改(大花貓動了哪些小玩具)。
改造下實體屬性
public virtual string Account { get { return _Account; } set { _Account = value; FieldTracking["Account"] = value; } }
看過編譯後的IL代碼的都知道,class中的屬性最終會編譯成兩個方法 setvalue和getvalue,那麼通過修改set方法添加FieldTracking["Account"] = value;就可以讓屬性在賦值的時候添加到字典中。
很簡單吧。
你以為這樣就完了。如果拿房間來比喻實體、拿玩具來比作屬性。我家那大花貓就是修改實體屬性的方法。你知道我家有多少玩具嗎?你每天回家的時候你知道大花貓動了哪個小玩具嗎?給每個玩具裝個GPS?哈哈哈哈,別鬧,花這心思還不如再買點回來。什嗎?買回來的還得裝,算了。研究下怎麼裝吧。
一個程式可能有上百個實體類,修改現有的實體類,給每個set加一行?作為一個程式員是不可能容忍做這樣的操作的。寫一個工具,讀取所有的實體代碼,加上這一行,儲存。這是個好辦法。那每次添加一個實體類就得調用工具重寫來一遍,每次修改屬性再調用一遍,恩。沒問題。能用就行。這不是一個真心養貓的人的人能容忍的。
那怎麼辦?把貓打死?那玩具的存在將會沒有任何意義。想到一個辦法,在我離開房子的時候(程式初始化),給房子裡的所有房間(實體類)建立一個同樣的房間(繼承),包含了與原房間所有需要監控(標記為virtual)的玩具的複製,在複製過程中加上GPS(-_~)。然後給貓玩。貓通過我給的門進到這個繼承的房間中玩所有玩具的時候,GPS就能將貓的動作全部記錄下來。我一回家,這貓玩了哪些玩具一看GPS記錄就全知道了。喲,這小崽子,在王元鵝呢。
看不懂,沒關係,上馬:
1、在程式集初始化的時候,通過反射,尋找所有繼承自BaseEntity的實體類。遍曆其中的屬性。找到標記為virtual進行複製。
剛開始對於如果找到virtual屬性花了不少時間。我總只想著在屬性上找,卻沒想到去set_value方法上去找(其實get_value方法也是)。還是太菜啊。
註:NoMapAttribute特性是一個自訂的標記,表示不參與映射。因為不參與映射就不需要監控。與本文章代碼沒有太大的關係。僅供參考。
//擷取實體所在的程式集(ClassLibraryDemo)var assemblyArray = AppDomain.CurrentDomain.GetAssemblies() .Where(w => w.GetName().Name == "ClassLibraryDemo") .ToList();//實體的基類var baseEntityType = typeof(BaseEntity);//迴圈程式集foreach (Assembly item in assemblyArray){ //找到這個程式集中繼承自基類的實體 var types = item.GetTypes().Where(t => t.IsAbstract == false && baseEntityType.IsAssignableFrom(t) && t != baseEntityType); foreach (Type btItem in types){ //遍曆這個實體類中的屬性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、根據1的結果,複製一個新的房間(動態代碼產生一個類,這個類繼承1中的實體,並且重寫了屬性的set方法)
這個過程就設計到動態代碼的產生了。
//首先建立一個與實體類對應的動態類CodeTypeDeclaration ct = new CodeTypeDeclaration(btItem.Name + "_Dynamic");//迴圈實體中的所有標記為virtual的屬性foreach (PropertyInfo fiItem in properties){//建立一個屬性var p = new CodeMemberProperty();//設定屬性為公用、重寫p.Attributes = MemberAttributes.Public | MemberAttributes.Override;//override//設定屬性的類型為繼承的屬性的資料類型p.Type = new CodeTypeReference(fiItem.PropertyType);//屬性名稱與繼承的一致p.Name = fiItem.Name;//包含set代碼p.HasSet = true;//包含get代碼p.HasGet = true;//設定get代碼//return base.Accountp.GetStatements.Add(new CodeMethodReturnStatement( new CodeFieldReferenceExpression( new CodeBaseReferenceExpression(), fiItem.Name)));//設定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"(加尾碼以示區分)
//聲明一個命名空間(與當前實體類同名+尾碼)CodeNamespace ns = new CodeNamespace(btItem.Namespace + ".Dynamic");ns.Types.Add(ct);
4、編輯產生代碼所在的程式集
//要動態產生代碼的程式集 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("錯誤清單:"); //編譯不通過 foreach (dynamic item in error) { Console.WriteLine("ErrorNumber:{0};Line:{1};ErrorText{2}", item.ErrorNumber, item.Line, item.ErrorText); } return; } else { Console.WriteLine("編譯成功。"); }
查看產生的程式碼
//查看產生的程式碼var codeText = new StringBuilder();using (var codeWriter = new StringWriter(codeText)){ CodeDomProvider.CreateProvider("CSharp").GenerateCodeFromNamespace(ns, codeWriter, new CodeGeneratorOptions() { BlankLinesBetweenMembers = true });}Console.WriteLine(codeText);
5、將複製的新類與原類建立映射關係。
foreach (Type item in ts){ //註冊(類比實現,通過字典實現的,也可以通過IOC注入方式處理) Mapping.Map(item.BaseType, item);}
6、獲得這個複製的實體物件
//建立一個指定的實體物件AccountEntity ae = Mapping.GetMap<AccountEntity>();
7、對這個實體物件的屬性進行賦值
//主鍵賦值不會修改屬性更新ae.BaseEntity_Id = 1;//不會變(未標記為virtual)ae.MainEntity_Name = "大花貓";ae.MainEntity_UpdateTime = DateTime.Now;//修改某個屬性ae.Account = "admin";ae.Account = "以最後一次的修改為準";
8、調用底層方法,底層根據這個實體屬性獲得被修改的屬性名稱
//調用基類中的方法 擷取變動的屬性var up = ae.GetFieldTracking();Console.WriteLine("有修改的欄位:");up.ForEach(fe =>{ Console.WriteLine(fe + ":" + ae[fe]);});
9、完美
就這樣,在底層就能知道哪些實體被賦值過了。
當然,有些實體我們只是需要用來計算,則可以調用方法將賦值過的屬性進行刪除
//刪除變更欄位ae.RemoveChanges("Account");
這隻是一個簡單的實現,還有一種比較複雜的情況,在第6步,獲得這個複製的實體物件時,怎麼用一個現有的new出來的實體物件去建立建並監控呢。就像,別人送我一房間現成的玩具,給我的時候貓就在裡面玩了。嗷,把貓打死吧。
總結:
再次認識到反射的強大。
也第一次實現了代碼產生代碼並使用的經曆。
對欄位和屬性的區別有了更深的認識。
對存取修飾詞和虛virtual方法有了更好的認識。