[转]EntityFramework走马观花之CRUD(中)
學(xué)習(xí)Entity Framework技術(shù)期間查閱的優(yōu)秀文章,出于以后方便查閱的緣故,轉(zhuǎn)載至Blog,可查閱原文:http://blog.csdn.net/bitfan/article/details/13023223
?
如果是獨立的實體對象,在底層數(shù)據(jù)庫中它對應(yīng)一張獨立的表,那么,對它進行新建、刪除和修改沒有任何難度,實在不值浪費筆墨在它上頭。
在現(xiàn)實項目中,完全獨立的對象少之又少,絕大多數(shù)情況都是對象之間有著緊密的關(guān)聯(lián)。這種關(guān)聯(lián)主要分為三種類型:一對一、一對多和多對多。
如果對EF淺嘗輒止,則我?guī)缀蹩梢钥隙阋欢〞趯嶋H開發(fā)中被對象間的關(guān)聯(lián)弄得焦頭爛額。下面就和大家聊聊EF是如何處理不同對象關(guān)聯(lián)類型數(shù)據(jù)更新問題的。
一對一關(guān)聯(lián)
在面向?qū)ο蟮氖澜缰?#xff0c;使用對象組合實現(xiàn)一對一關(guān)聯(lián),這種關(guān)聯(lián)具有方向性。比如A與B對象間是一對一關(guān)聯(lián),如果得到A就能順藤摸瓜得到B,就說A可導(dǎo)航到B,這是單向一對一關(guān)聯(lián),如果從A能得到B,從B也能得到A,這就是雙向一對一關(guān)聯(lián)。
以下代碼中,Teacher與Office間是一對一雙向關(guān)聯(lián):
?
public partialclass Teacher{public int TeacherId { get; set; }public string Name { get; set; }public virtual Office Office { get; set; }}public partialclass Office{public int OfficeId { get; set; }public string Address { get; set; }public virtual Teacher Teacher { get; set; }}?
?
User與Office類間的關(guān)聯(lián)在數(shù)據(jù)庫中體現(xiàn)為Teacher和Office兩個表間的一對一關(guān)聯(lián)。
這里要注意:面向?qū)ο笫澜缰袑嶓w類的一對一雙向關(guān)聯(lián),雙方的地位是“平等”的,但在關(guān)系數(shù)據(jù)庫領(lǐng)域中,每個關(guān)聯(lián)一定有主有次,下圖為SQL Server中創(chuàng)建關(guān)聯(lián)的窗體,可以看到一對一關(guān)聯(lián)兩端分別是主鍵表和外鍵表。
?
?
在數(shù)據(jù)庫中創(chuàng)建一對一關(guān)聯(lián),有兩個很要命的東西,不注意鐵定出問題:
(1)在外鍵表一端,其Key不能是自增標(biāo)識字段。比如Office作為從表,其主鍵OfficeId就不能是自增的,否則EF會在向Office添加記錄時報告:Adependent property in a ReferentialConstraint is mapped to a store-generatedcolumn.
(2)應(yīng)該把一對一關(guān)聯(lián)設(shè)置為“級聯(lián)刪除”。否則在刪除主鍵表端記錄時如果外鍵表端記錄還存在,則會報錯。
還有另外一個小問題:
數(shù)據(jù)庫中的一對一關(guān)聯(lián),總被EF識別為1 ~ 0..1關(guān)聯(lián),即使你使用ModelFirst方式顯式定義Teacher和Office類之間為1 ~ 1關(guān)聯(lián),生成數(shù)據(jù)庫后,再用Database First方式逆向生成數(shù)據(jù)模型,你會發(fā)現(xiàn)Teacher和Office類的關(guān)聯(lián)變成1 ~ 0..1 !
換句話說,EF實體對象的1對1關(guān)聯(lián),總是被張冠李戴為1對0..1關(guān)聯(lián)。
好了,下面具體說說新建記錄的問題。
要在數(shù)據(jù)庫中創(chuàng)建一對一關(guān)聯(lián)實體對象的相關(guān)記錄,可以使用以下三種方式:
(1)new 一個Teacher對象,new一個Office對象,用代碼關(guān)聯(lián)兩者,之后SaveChanges。只要OfficeId列不是自增標(biāo)識列就能成功。
(2)new 一個Teacher對象,SaveChanges,再new一個Office對象,將其關(guān)聯(lián)上這個“老”的己存在數(shù)據(jù)庫中的Teacher對象,SaveChanges,也能成功。
(3)new一個Office對象,直接SaveChanges,這時會報錯。因為Office是從對象,它沒有關(guān)聯(lián)主對象時,是無法保存到數(shù)據(jù)庫的。
?
下面看看刪除對象的情形:
(1)刪除Teacher對象,SaveChanges,則它關(guān)聯(lián)的Office也會被同時刪除,注意這要求啟用“級聯(lián)刪除”特性,否則,必須先從DbSet<Office>中移除Office對象,再刪除Teacher,操作才能成功。
(2)直接刪除Office對象,SaveChanges:沒有任何問題。
?
輪到修改一對一關(guān)聯(lián)的情形了。
假設(shè)我們需要給一個Teacher對象“換”另一個“Office”對象(即換辦公室,估計這位老師升官了)。
這有兩種方式實現(xiàn):
第一種方式很簡單,直接找到Teacher對象所關(guān)聯(lián)的Office對象,修改它的屬性為新值,之后SaveChanges即可,這本質(zhì)上是“舊房改造”。
另一種方式是“直接分配新房”: 先new一個Office對象代表新辦公室,再把它關(guān)聯(lián)上Teacher,如下所示:
?
using (varcontext = new EFModelFirstDbEntities()){//查找要換辦公室的Teacher對象 Teacher teacher = context.Teachers.Include("Office").First();teacher.Office = new Office{Address = "Bit" +ran.Next(1, 100)}; context.SaveChanges();}?
這里有一個細節(jié)很重要,在提取Teacher對象時,一定要使用Include方法把它相關(guān)聯(lián)的“Office”對象也裝入進來。否則,在SaveChanges時,EF報告:
違反了 PRIMARY KEY 約束“PK_Office”。不能在對象“dbo.Office”中插入重復(fù)鍵。
?如果確實不想Include,則可以顯式指示EF把老的Office對象狀態(tài)設(shè)置為“刪除”:
?
Teacher teacher =context.Teachers.First();context.Entry(teacher.Office).State = EntityState.Deleted;teacher.Office = new Office{Address = "Bit" +ran.Next(1, 100)};context.SaveChanges();?
?
有的朋友可能會推測EF會生成兩條SQL命令,一條是Delete,另一條是Insert,但我最終測試的結(jié)果可能會讓你吃驚:
上述兩種方式,最后生成的都是一條Update命令。
這就是說,這兩種實現(xiàn)方式?jīng)]有本質(zhì)差別。
發(fā)發(fā)感觸:開發(fā)中有很多細節(jié),光看資料不動手是發(fā)現(xiàn)不了的。光看書不實踐能真正掌握技術(shù)?那是神話!開發(fā)實踐是學(xué)習(xí)軟件技術(shù),提升自身能力的硬道理。
一對多關(guān)聯(lián)
有了一對一關(guān)聯(lián)的基礎(chǔ),把握一對多關(guān)聯(lián)就簡單多了。
我選取“Book(書)”和“BookReview(書評)”作為一對多關(guān)聯(lián)的示例,并在數(shù)據(jù)庫中為關(guān)聯(lián)啟用了“級聯(lián)刪除”特性:
?
?
如果使用Db First方式,可以得到以下實體類代碼:
?
public partial class Book{public Book(){this.BookReviews = new HashSet<BookReview>();}public int BookId { get; set; }public string BookName { get; set; }public virtual ICollection<BookReview> BookReviews { get; set; }}public partial class BookReview{public int BookReviewId { get; set; }public string Reader { get; set; }public string Content { get; set; }public int BookId { get; set; }public virtual Book Book { get; set; }}?
上述代碼中,最值得注意的就是Book類的BookReviews屬性是HashSet。
?
先看新建記錄的情況:
對于一對多關(guān)聯(lián),new一個Book對象,SaveChanage,之后,就可以向其BookReviews集合屬性中添加新書評對象。
這很簡單,不用多說。刪除記錄的情況就有點復(fù)雜。
?如果要刪除單個的Book對象,由于啟用了級聯(lián)刪除,干掉一個Book,它所關(guān)聯(lián)的所有BookReview也一并刪除了。
如果想刪除單個書評,如果使用DB_First方式,Visual Studio生成的實體對象集合其類型為ICollection<T>,實際上是一個普通的HashSet<T>集合對象,不具備跟蹤對象狀態(tài)的功能。因此,在刪除單個對象時,需要顯式設(shè)置其狀態(tài)為EntityState.Deleted,否則,刪除將失敗:
?
using(var context=newEFModelFirstDbEntities()){Book book =context.Books.First();BookReview reviewToBeDelete = book.BookReviews.FirstOrDefault();context.Entry(reviewToBeDelete).State =EntityState.Deleted;book.BookReviews.Remove(reviewToBeDelete);context.SaveChanges();}?
?
更簡單的方式是直接從DbSet中移除,這是推薦的方式
?
Book book = context.Books.First();BookReview reviewToBeDelete =book.BookReviews.FirstOrDefault();context.BookReviews.Remove(reviewToBeDelete);context.SaveChanges();?
?
另一個在實踐中遇到的問題是”批量刪除”。
?
如果想刪除某本書的全部書評,像下面這么干是不行的,運行時將拋出異常:
?
foreach (var review in book.Reviews){context.BookReviews.Remove(review);}?
?
這是因為循環(huán)迭代訪問集合的過程中不允許修改集合。
然而,可以使用ToList()方法將DbSet()中的實體集合提取為內(nèi)存集合,然后foreach訪問時,從“原始對象集合”中移除:
?
var reviewList = book.Reviews.ToList();foreach (var review in reviewList){context.BookReviews.Remove(review);}?
?
這里reviewList和context.BookReviews是兩個不同的集合對象,在foreach訪問reviewList時,可以方便地移除BookReviews中的對象。
上述兩個“不同”的集合,實際引用“相同的”一堆BookReview對象,不信?
設(shè)斷點讓程序暫停,以下在VisualStudio“立即窗口(Immediate Windows)”測試可以證明我沒有說謊:
?
?reviewList==book.Reviewsfalse?reviewList[0]==book.Reviews[0]True?
?
關(guān)于批量刪除,默認情況下,EF會為每個刪除的實體對象生成一條Delete命令,當(dāng)刪除大量實體時,這有可能帶來性能問題。在這種情況下,最好的解決方案是直接向數(shù)據(jù)庫發(fā)送SQL命令:
?
Book book = context.Books.First();context.Database.ExecuteSqlCommand( "Delete from BookReview where BookId={0}" , book.BookId);?
?
一個SQL命令搞掂,簡單高效。可以收工,回家睡覺去也!
?關(guān)于一對多關(guān)聯(lián),還有一個問題需要說說,那就是“如何在兩個實體對象間移動子對象”。實現(xiàn)起來也簡單:
先從源實體對象中先Remove掉子對象,再Add到目標(biāo)對象中即可。
以下示例代碼把第一本書的第一條書評移到第二本書下:
?
Book book1 = context.Books.First();Book book2 = context.Books.OrderBy(b =>b.BookId).Skip(1).First();BookReview review = book1.BookReviews.First();book1.BookReviews.Remove(review);book2.BookReviews.Add(review);context.SaveChanges();與一對一關(guān)聯(lián)的情況類似,上述代碼將只生成一個update命令。
多對多關(guān)聯(lián)
多對多關(guān)聯(lián)的最典型實例就是軟件權(quán)限管理系統(tǒng)中的“用戶(User)”與“角色(Role)”。一個用戶可以擁有多種角色,一個角色可以包容多個用戶。
?
對于實體對象間的多對多關(guān)聯(lián),EF code First自動地在數(shù)據(jù)庫層將其拆成兩個一對多關(guān)聯(lián),變成以下這個模樣,并為這兩個關(guān)聯(lián)啟用“級聯(lián)刪除”:
?
?
多對多關(guān)聯(lián)對象的新建和修改,可以參照一對多的情形來處理。比如要刪除單個的User或Role對象,很簡單,直接從DbSet中移除它們,再SaveChange即可。
不再多說,大家自己回去編寫實驗代碼。
比較麻煩的是涉及到多對多關(guān)聯(lián)本身的添加與刪除問題。典型的例子是:
把某個User加入到特定的Role的User集合中,或者從Role的User集合中移除某個User。
對于上述這些情況,EF將不動User表和Role表,只在中間表RoleUsers中動手腳。
比如要從User與Role多對多關(guān)聯(lián)任一方的對象集合導(dǎo)航屬性中移除某個(或某些)User或Role對象,然后SaveChanges,對于這種操作,EF默認不會修改User與Role實體對象的狀態(tài),只是在底層數(shù)據(jù)庫的中間表RoleUsers中刪除了相應(yīng)的記錄,使它們“斷開關(guān)聯(lián)”。
理解這點很關(guān)鍵。
這里還有一個細節(jié):
如果在SaveChange之后,涉及到的相關(guān)實體對象(User和Role)都還需要繼續(xù)使用,則一定要注意:你僅從一方集合導(dǎo)航屬性中移除了某對象,另一方的集合導(dǎo)航屬性中對此對象的引用是還在的,這有可能帶來不一致的問題。只有等到下一次從數(shù)據(jù)庫中裝入數(shù)據(jù)時,才能重回一致。
請看以下代碼:
using (varcontext = new EFCodeFirstDbContext()){User u =context.Users.Find(userId);Role r =context.Roles.Find(roleId);if (u != null && r !=null){//以下兩句,任一種方式均可行 r.Users.Remove(u);//u.Roles.Remove(r);int result =context.SaveChanges();Console.WriteLine("將用戶從角色中移除操作完畢。返回值:" + result);}}?
上述加粗的兩句,不管寫哪一句,SaveChanges之后,都能達到在底層數(shù)據(jù)庫的RoleUsers表中刪除相應(yīng)記錄的功能。但如果只寫了一句,則Role對象的Users集合中確實移除了指定的User對象,但此User對象的Roles屬性中還包容有這個Role對象,這就是數(shù)據(jù)不一致的情況。因此,最安全的寫法是兩句都寫。
總結(jié)
以上是生活随笔為你收集整理的[转]EntityFramework走马观花之CRUD(中)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Auto login to your c
- 下一篇: Oracle 基础篇 --- 索引选项