EntityFramework进阶——数据编辑与维护
實體數據對象狀態
在EF環境下,應用程序更改數據對象會引發數據集狀態的變更,可能的狀態有以下幾種:
| Added | 添加實體對象創建到實體集中,數據未添加進數據庫 |
| Modified | 實體對象已經存在于實體數據集中,數據庫同時存在對應的數據,但某些實體對象屬性值已經改變,未更新到數據庫中 |
| Deleted | 實體對象已經存在于實體數據集中,數據庫同時存在對應的數據,但是實體對象本身被標識為刪除 |
| Unchanged | 實體對象存在于數據集中,數據庫同時包含對應未曾更改的相同數據 |
| Detached | 實體對象已經不在數據集中 |
當SaveChanges方法被調用時:
1、Added狀態會將新實體對象的屬性數據更新到數據庫。
2、Modified狀態實體對象的屬性值就會逐一更新到數據庫中對應的數據字段。
3、Deleted則將其中對應實體對象的數據刪除。
4、當實體對象為Added或者Modified狀態時,調用SaveChanges方法更新數據成功后,會將狀態調整為Unchanged。
5、當實體對象為Deleted狀態時,調用SaveChanges方法更新數據成功后,會將對象的狀態調整為Detached。
?
可以使用DbContext.Entry方法獲取對象的狀態。
下面有代碼說明舉例,假設存在如下實體類:
public class Product{public int Id { get; set; }public string Name { get; set; }public string Category { get; set; }public int Price { get; set; }}上下文類代碼如下圖所示:
public class SaveChangesModel : DbContext{public SaveChangesModel(): base("name=SaveChangesModel"){}public virtual DbSet<Product> Product { get; set; }}運行程序建立相應的數據庫和表結構后,修改Main函數中的代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesModel db = new SaveChangesModel()){Product product = new Product{Name = "電腦",Category = "辦公用品",Price = 5000,};Console.WriteLine("初始狀態:{0}", db.Entry(product).State.ToString());db.Product.Add(product);Console.WriteLine("Add后的狀態:{0}", db.Entry(product).State.ToString());Console.ReadKey();} }運行后效果如下圖所示:
? ? ? ? ? ? ? ? ? ? ???
可以看到,一開始product對象創建完成后是Detached狀態,表示其與數據庫無任何關聯,調用Add方法后,轉化成為Added狀態,表示這一組數據對象當前是準備加入數據庫的狀態。
EF根據數據對象的狀態決定是否進行底層數據庫的變更,并且通過狀態調整達到數據變更的目的。
可以將其中代碼部分修改下,如下圖所示:
//db.Product.Add(product); db.Entry(product).State = System.Data.Entity.EntityState.Added;其中直接將Product對象的state設置為Added,效果與Add方法相同,因此不需要明確調用Add方法,只需要通過狀態標識即可完成添加操作。?
更新與刪除?
?更新和刪除操作必須先要找到數據集中相應的數據才能進行操作。
Find()方法可以利用數據表的主鍵進行快速查詢出想要的數據。更新操作的示例代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);Console.WriteLine("初始狀態:{0}", db.Entry(product).State.ToString());product.Price = 4500;Console.WriteLine("修改后狀態:{0}", db.Entry(product).State.ToString());db.SaveChanges();Console.ReadKey();} }運行結果如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
?
?刪除效果的實例代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);Console.WriteLine("初始狀態:{0}", db.Entry(product).State.ToString());db.Product.Remove(product);Console.WriteLine("刪除后狀態:{0}", db.Entry(product).State.ToString());db.SaveChanges();Console.ReadKey();} }運行結果下圖所示:?
? ? ? ? ? ? ? ? ? ? ? ? ? ??
?Attach
DbSet類定義了一個Attach方法,此方法定義接收一個實體數據對象參數,將其附加到數據集中,等同于將此數據直接從數據庫取出并轉換成對應的數據對象,而附加完成之后,entity的狀態時Unchanged,通過修改其狀態,即可通過SaveChanges方法變成變更更新操作。
修改Main函數中的方法如下圖所示:
static void Main(string[] args) {using (SaveChangesModel db = new SaveChangesModel()){Product product = new Product{Id = 1,Name = "電腦",Category = "辦公用品",Price = 6000,};Console.WriteLine("初始狀態:{0}", db.Entry(product).State.ToString());db.Product.Attach(product);Console.WriteLine("Attach后狀態:{0}", db.Entry(product).State.ToString());db.Entry(product).State = System.Data.Entity.EntityState.Modified;Console.WriteLine("Modified后狀態:{0}", db.Entry(product).State.ToString());Console.ReadKey();} }運行后結果如下圖所示:?
? ? ? ? ? ? ? ? ? ? ?
?
變更追蹤——DbContext.ChangeTracker?
DbContext會對實體的更新操作進行追蹤,如果想要存取變更狀態的信息,可以通過DbContext.ChangeTracker屬性的調用來獲取支持追蹤功能的DbChangeTracker對象,語句如下:
DbChangeTracker tracker = context.changeTrackerDbChangeTracker定義了Entries方法,執行這個方法返回的是一個當前DbContext追蹤的實體對象,其相關的IEnumerable<DbEntityEntry>集合,進一步調用此方法即可逐一取出所有的DbEntityEntry對象,并提取所需的實體對象變更信息。?
示例代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);DbChangeTracker tracker = db.ChangeTracker;EntryInfo(tracker, "首次載入");product.Name = "惠普電腦";product.Price = 3000;EntryInfo(tracker,"修改一項商品數據的名稱");db.Product.Add(new Product{Name = "鼠標",Price = 19,Category = "辦公用品",});EntryInfo(tracker, "添加一項商品數據");var product2 = db.Product.Find(2);db.Product.Remove(product2);EntryInfo(tracker,"刪除一項商品數據");Console.Read();} }static void EntryInfo(DbChangeTracker tracker,string desc) {Console.WriteLine("\n{0}: ",desc);Console.WriteLine("".PadRight(48, '.'));IEnumerable<DbEntityEntry> entries = tracker.Entries();foreach (DbEntityEntry entry in entries){Console.WriteLine("變更實體:{0}",entry.Entity.GetType().FullName);EntityState state = entry.State;Console.WriteLine("狀態:{0}", state);if (state != EntityState.Deleted){if (state != EntityState.Added){PropertyList(entry.GetDatabaseValues(),"當前數據庫中的副本");PropertyList(entry.OriginalValues,"屬性值");}if (state != EntityState.Unchanged){PropertyList(entry.CurrentValues, "變更后的屬性值");}Console.WriteLine("//");}} }static void PropertyList(DbPropertyValues values,string desc) {Console.WriteLine("{0}:", desc);foreach (string name in values.PropertyNames){Console.WriteLine("\t{0}:{1}", name, values[name]);} }修改實體部分的運行結果:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
新增實體部分的運行結果:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
刪除實體部分的運行結果:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
?
?更新驗證異常——DbEntityValidationException
數據進入數據庫之前,很有可能發生各種更新錯誤,因此必須進行各種驗證以確保正確性。當我們執行SaveChanges方法進行底層數據更新操作時,EF會根據實體類的屬性逐一進行驗證,以決定是否執行數據的更新操作,避免錯誤數據進入數據庫。一旦出現驗證不符的數據內容,就會產生DbEntityVaildationException異常。
下面通過例子說明,新建控制臺項目SaveChangesEX,并添加如下實體類:
[Table("Product")]public class Product{[Key]public int Id { get; set; }[Required]public string Name { get; set; }[Range(100,5000)]public int Price { get; set; }[Range(100,5000)]public int SPrice { get; set; }}上下文類代碼如下圖所示:
public class SaveChangesEXModel : DbContext{public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }}首次運行項目在數據庫中建立的表結構如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ??
?
修改Main函數中的代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();}catch(Exception ex){Console.WriteLine("\n錯誤信息:{0}\n",ex.Message);}Console.ReadKey();} }以上代碼創建了一個新的Product對象,并將其name屬性賦值為null,?這是無法通過驗證的,同理,Price屬性和SPrice屬性。
運行效果如下圖所示:
?
下面進一步擴展catch子句中的代碼:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();}catch(Exception ex){Console.WriteLine("\n錯誤信息:{0}\n",ex.Message);if (ex is DbEntityValidationException){foreach (DbEntityValidationResult validationResult in ((DbEntityValidationException)ex).EntityValidationErrors){foreach (DbValidationError error in validationResult.ValidationErrors){Console.WriteLine(".....{0}",error.ErrorMessage);}}}}Console.ReadKey();} }DbEntityValidationException.EntityValidationErrors屬性返回與此實體對象有關的驗證錯誤,每一個驗證錯誤封裝為DbEntityValidationResult?對象,最后形成IEnumerable<DbEntityValidationResult>返回,因此我們可以通過foreach循環列舉其中所有的DbEntityValidationResult 對象。
每一個實體相關的驗證錯誤進一步封裝在DbEntityValidationResult.ValidationErrors屬性返回的ICollection<DbVaildationError>集合中,通過其中的DbValidationError.ValidationErrors可以取出真正的信息。
如果想要同時獲取造成這個錯誤的關鍵屬性。可以調用DbValidationError.PropertyName以獲取屬性名稱。
運行效果如下圖所示:
? ? ? ? ? ? ? ? ?
上述輸出結果中,除了原來的信息外,同時還列出了每一組屬性專屬的錯誤信息,這些信息是屬性數據注解內置的默認消息正文。如果需要設置ErrorMessage指定輸出信息,可以修改Product.cs如下所示:
[Table("Product")]public class Product{[Key]public int Id { get; set; }[Required(ErrorMessage ="必須指定商品名稱")]public string Name { get; set; }[Range(100,5000,ErrorMessage ="商品價格范圍10-500")]public int Price { get; set; }[Range(100,5000,ErrorMessage ="商品特價范圍10-500")]public int SPrice { get; set; }}運行效果如下圖所示:
? ? ? ? ? ? ? ? ??
?
覆寫DbContext.ValidateEntity方法
可以覆寫DbContext.ValidateEntity方法自定義驗證程序,以進一步調整輸出驗證結果,修改上下文類代碼如下圖所示:
public class SaveChangesEXModel : DbContext {public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items){var list = new List<DbValidationError>();if (entityEntry.CurrentValues.GetValue<string>("Name") == null || entityEntry.CurrentValues.GetValue<string>("Name").Length < 4){list.Add(new DbValidationError("Name", "商品名稱必須多于四個字符"));}if (entityEntry.CurrentValues.GetValue<int>("Price") < 0){list.Add(new DbValidationError("Price","商品價格不得小于0"));}if (entityEntry.CurrentValues.GetValue<int>("SPrice") < 0){list.Add(new DbValidationError("SPrice", "商品特價不得小于0"));}if (list.Count() > 0){return new DbEntityValidationResult(entityEntry, list);}else{return base.ValidateEntity(entityEntry, items);}} }再次運行程序會得到如下結果:
? ? ? ? ? ???
覆寫SaveChange方法?
在EF環境下,數據變更是最后調用SaveChanges方法將數據正式更新到數據庫,如果在更新過程中有需要執行的程序代碼,可以嘗試直接在實體模型中覆寫這個方法。
我們可以將處理驗證異常的相關代碼覆寫到SaveChange方法中,如此一來,就不需要每次調用SaveChange時處理相關問題。
修改上下文代碼如下圖所示:
public class SaveChangesEXModel : DbContext{public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }public override int SaveChanges(){try{return base.SaveChanges();}catch (Exception ex){string message = ex.Message;if (ex is DbEntityValidationException){message = "驗證異常\n";foreach (var validationResult in ((DbEntityValidationException)ex).EntityValidationErrors){foreach (var error in validationResult.ValidationErrors){message += ("......" + error.ErrorMessage + "\n");}}}throw new Exception(message);} }}修改Main方法中代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();Console.WriteLine("添加了{0}項數據!", count);}catch(Exception ex){Console.WriteLine("\n錯誤信息:{0}\n", ex.Message);}Console.ReadKey();} }運行結果如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ???
?
?
?SQL語句
EF支持SQL語句以便我們隨時進行數據的增刪改查操作。示例代碼如下圖所示:
using System.Data.Entity.Infrastructure;static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT * FROM dbo.Product";DbSqlQuery<Product> query = db.Product.SqlQuery(sql);List<Product> products = query.ToList();Console.WriteLine("商品項數:{0}",products.Count());foreach (Product product in products){Console.WriteLine("{0}\t售價:{1},特價:{2}",product.Name,product.Price,product.SPrice);}Console.ReadKey();} }運行結果如下圖所示:
? ? ? ? ? ? ? ? ? ? ? ? ??
?
注意以下幾點:
1、SQl語句語法必須正確,?不然報錯。
2、SQl語句得到的屬性值個數必須和指定的類型的屬性個數相同。比如,select Name,Price From Product 這句Sql中只有Name和Price兩個屬性,無法順利轉化成Product對象,故會報錯。
?
當查詢結果屬性個數不同時可以使用如下方法,修改Main函數中代碼如下圖所示:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT Name FROM dbo.Product";DbRawSqlQuery<string> query = db.Database.SqlQuery<string>(sql);//此方法List<string> productsName = query.ToList();Console.WriteLine("商品項數:{0}", productsName.Count());foreach (string productName in productsName)Console.WriteLine("{0}", productName);}Console.ReadKey(); }如果需要返回多個字段,可以預先創建對應的類進行轉換,如下圖所示:
public class SProduct{public int Id { get; set; }public string Name { get; set; }public int Price { get; set; }}修改Main函數中的代碼:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT Name,Price FROM dbo.Product";DbRawSqlQuery<SProduct> query = db.Database.SqlQuery<SProduct>(sql);List<SProduct> products = query.ToList();Console.WriteLine("商品項數:{0}", products.Count());foreach (SProduct product in products){Console.WriteLine("{0} ,價格:{1}", product.Name,product.Price);}Console.ReadKey();} }?
在SQL語句中使用參數
不當的SQL語句很容易遭受黑客的注入攻擊,我們可以使用參數動態建立所需要的SQL語句。代碼如下:
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){SqlParameter P0 = new SqlParameter("P0",8);SqlParameter P1 = new SqlParameter("P1", "%移動%");object[] parameters = { P0,P1 };string sql = "select Id,Name,Price From Product Where Id >@P0 AND Name LIKE @P1 ";DbRawSqlQuery<SProduct> query = db.Database.SqlQuery<SProduct>(sql, parameters);List<SProduct> Products = query.ToList();} }上述SQL語句查找Id大于8,并且名字包含“移動”的數據。
執行非查詢變更指令——ExecuteSqlCommand?
Update、InsertInto、delete等更新操作必須通過ExecuteSqlCommand方法,而且不會返回任何數據集。語句如下:
int count = context.Database.ExecuteSqlCommand(sql)上述的程序代碼返回變更的數據項,count 記錄了被更新的項數。
using (SaveChangesEXModel db = new SaveChangesEXModel()) {string sql = "UPDATE Product SET SPrice = -1 Where Price > 100";int count = db.Database.ExecuteSqlCommand(sql);Console.WriteLine("更新數據項數:{0}",count);Console.ReadKey(); }同樣可以在SQL語句中使用參數:
using (SaveChangesEXModel db = new SaveChangesEXModel()) {SqlParameter P0 = new SqlParameter("P0", -1);SqlParameter P1 = new SqlParameter("P1", 100);object[] parameters = { P0, P1 };string sql = "UPDATE Product SET SPrice = @P0 Where Price > @P1";int count = db.Database.ExecuteSqlCommand(sql,parameters);Console.WriteLine("更新數據項數:{0}",count);Console.ReadKey(); }?
使用Local查詢追蹤本地數據集
可以使用DbSet.Local屬性返回當前系統本地的數據內容,也就是DbContext對象追蹤的數據集內容。
參考如下代碼:
context.Product.Count();
context.Product.Local.Count();
第一條會送出SQL語句查詢,返回數據庫中對應的數據表中所儲存的記錄條數。第二行則是針對本地的DbSet對象讀取其中的數據項數(或者記錄條數),由于沒有執行查詢,DbContext并沒有獲取任何數據進行處理,因此,第二條的值為0.
static void Main(string[] args) {using (SaveChangesEXModel db = new SaveChangesEXModel()){var count = db.Product.Count();Console.WriteLine("數據庫中數據的條數:{0}",count);try{Product product = new Product{Name = "茶杯",Price = 10,SPrice = 5,};db.Product.Add(product);Console.WriteLine("本地的數據條數:{0}", db.Product.Local.Count);}catch (Exception ex){Console.WriteLine("\n錯誤信息:{0}\n", ex.Message);}Console.ReadKey();} }運行結果如下圖所示:
? ? ? ? ? ? ? ???
?
local查詢?
EF只有真正取出數據內容時才會進行SQL語句的傳送,而這個過程是EF自動執行的,因此不當的設計容易造成重復查詢,引發嚴重的性能問題,若要避免這種情況,可以嘗試通過Local獲取載入的數據,往后對Local數據進行操作,以減少SQL查詢操作。
通過以下程序代碼來說明:
using(var context = new KTStoreModel) {var products = context.product.Where(p => p.price > 100);Product firstProduct = products.First();Console.WriteLine("\n第一項商品數據名稱:{0}\n",firstProduct.Name);Console.Writeline("\n所有商品數據列表:\n");foreach(var product in products){Console.WriteLine(Product.Name);}Console.Read() }上述代碼會導致SQL語句執行兩次,第一次是查詢第一項商品數據,第二次是查詢所有商品數據。
改寫代碼,將其中Where方法的調用調整如下:
context.Product.Where(p => p.price > 100).Load(); var products = context.product.local;第一次直接調用Load方法預先將數據載入DbContext,并且通過Local屬性的引用取回載入的數據內容并存儲于Products變量中。由于直接查詢Local屬性獲取本地數據,因此不需要重復執行SQL語句,這對于性能的提升會有一定幫助。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的EntityFramework进阶——数据编辑与维护的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NPM管理
- 下一篇: 独立磁盘冗余阵列:RAID