EF 7 Code First


加載方式三種

1. Eager Loading

2. Lazy Loading

3.Explicit Loading

使用EF在與關系型數據庫的交互中不可避免地需要加載數據,如何加載數據變得至關重要。你可以設想在一個包含數十萬條數據的表中,你如何通過EF來加載數據呢?一次性將所有數據載入服務器內存或者在循環中一遍又一遍地分步加載數據?使用什么樣的數據加載方式需要具體問題具體分析,我們不能在這里籠統地下決定說哪種方式好哪種方式不好。但有一點是需要遵循的,那就是如何提高數據加載的效率。EF提供了幾種不同的數據加載方式,我們可以根據不同的需要靈活使用它們。

  先簡單說一下如何創建環境。如果你對這些步驟了如指掌,請直接跳過。

1. 在Visual Studio中創建一個示例工程。最簡單的莫過於ConsoleApplication

2. 在工程中添加ADO.NET Entity Data Model。如下圖所示,

  選擇其中的兩個表作為示例,表Teacher和表Course,關系如下,

  添加edmx之后,Visual Studio為自動幫我們生成/添加所有需要的文件和內容,然后我們就可以開始在代碼中操作數據庫了。來看看在EF中幾種不同的數據加載方式。

 

惰性加載(Lazy Loading)

  默認情況下,EF會使用惰性加載方式加載數據,即ctx.Configuration.LazyLoadingEnabled = true; 在下面的代碼中,外層循環會執行一次查詢,並將返回的結果存放在變量q中。而內層循環會在每一次循環過程中獨立進行查詢,所以,如果數據庫表Teacher中有100條記錄而Course有1000條記錄,那么整個過程將產生1001次查詢。

復制代碼
using (var ctx = new SchoolDBEntities()) { var q = from t in ctx.Teachers select t; foreach (var teacher in q) { Console.WriteLine("Teacher : {0}", teacher.TeacherName); Console.WriteLine("Respective Courses..."); foreach (var course in teacher.Courses) { Console.WriteLine("Course name : {0}", course.CourseName); } Console.WriteLine(); Console.ReadKey(); } }
復制代碼

  下面是程序執行的結果以及在SQL Server Profiler中的跟蹤記錄。可以清楚地看到,對表Teacher只進行了一次查詢,由於該表只有8條記錄,於是在內層循環中又分別產生了8次對表Course的查詢。

  在某些場合下,這種情況是可以接受的。你完全可以根據需要來控制內層循環何時顯示加載數據,或者根本不加載數據。但是,在分層結構的應用程序中,上述代碼結構並不適用,因為內層循環需要依賴於外層的Context,也就是說它們是在同一個數據庫上下文中完成的,如果嘗試將內層循環的代碼移到外面或者其它類中,則它將獲取不到任何數據。

 

顯式加載(Explicit Loading)

  如果你想人為控制惰性加載的行為,可以嘗試使用下面的代碼。首先需要手動關閉EF的惰性加載,通過代碼ctx.Configuration.LazyLoadingEnabled = false;來完成。

復制代碼
using (var ctx = new SchoolDBEntities()) { ctx.Configuration.LazyLoadingEnabled = false; var q = from t in ctx.Teachers select t; foreach (var teacher in q) { Console.WriteLine("Teacher : {0}", teacher.TeacherName); Console.WriteLine("Respective Courses..."); // Conditionally load the child data if (true) { ctx.Entry(teacher).Collection(c => c.Courses).Load(); } foreach (var course in teacher.Courses) { Console.WriteLine("Course name : {0}", course.CourseName); } Console.WriteLine(); Console.ReadKey(); } }
復制代碼

  注意內層循環只有在上面高亮顯示部分的代碼執行之后才會獲取到數據,否則返回結果為0。通過添加判斷條件,我們可以對數據加載方式進行控制,從而有效地減少程序與數據庫交互的次數。大多數情況下,我們從數據庫獲取到的數據並不都是有用的,如果每次只有很少一部分數據有用,那么我們為什么不過濾掉那些無用的數據從而盡量較少數據交互的次數呢?

 

預先加載(Eager Loading)

  如果你想讓所有數據一次性全部加載到內存中,那么你需要使用.Include(Entity)方法。看下面的代碼,

復制代碼
using (var ctx = new SchoolDBEntities()) { var q = from t in ctx.Teachers.Include("Courses") select t; foreach (var teacher in q) { Console.WriteLine("Teacher : {0}", teacher.TeacherName); Console.WriteLine("Respective Courses..."); foreach (var course in teacher.Courses) { Console.WriteLine("Course name : {0}", course.CourseName); } Console.WriteLine(); Console.ReadKey(); } }
復制代碼

  如果你查看SQl Server Profiler中的跟蹤信息,你會發現只有一次數據交互過程,即程序只通過一次查詢便獲取到了所有需要的數據。在分層結構中,該方法是最容易的,我們可以將數據庫底層獲取到的結果返回給上層,它不具有任何依賴項。同時,它也可以減少程序與數據庫的交互次數。不過仍然有缺點,那就是如果數據量較大,一次性將所有數據載入內存往往並不是最明智的選擇。.Include(Entity)方法允許級聯使用,你可以預先加載具有多層級結構的數據。

  就像前面所說,選擇什么樣的數據加載方式需要因時而異,每一種數據加載方式都有它存在的意義,但目的只有一個,那就是以最少的代價獲取到需要的數據。

 

數據庫表與實體Entity的映射

實現方式有兩種,最終是通過外鍵的方式實現一對一 一對多 多對多的關聯,

實體直接導航關聯或外鍵,導航實現實體關系清晰,由EF Code First自動生成關聯外鍵 DataAnnonation FluentAPI更底層,實體里嵌套實體,實體被默認為ComplexType

1.DataAnnonation 和 類嵌套關系結合

2.FluentAPI

 Virtual定義的導航屬性只為延遲加載

如果你對EF里實體間的各種關系還不是很熟悉,可以看看我的思路,能幫你更快的理解。

I.實體間一對一的關系       

添加一個PersonPhoto類,表示用戶照片類

/// <summary> /// 用戶照片類 /// </summary> public class PersonPhoto { [Key] public int PersonId { get; set; } public byte[] Photo { get; set; } public string Caption { get; set; } //標題 public Person PhotoOf { get; set; } }

當然,也需要給Person類添加PersonPhoto的導航屬性,表示和PersonPhoto一對一的關系:

public PersonPhoto Photo { get; set; }

直接運行程序會報一個錯:

Unable to determine the principal end of an association between the types ‘Model.Per-sonPhoto’ and ‘Model.Person’. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

思考:為何第一節的Destination和Lodging類直接在類里加上導航屬性就可以生成主外鍵關系,現在的這個不行呢?        解答:之前文章里的Destination和Lodging是一對多關系,既然是一對多,EF自然就知道設置Destination類的DestinationId為主鍵,同時設置Lodging類里的DestinationId為外鍵;但是現在的這個Person類和PersonPhoto類是一對一的關系,如果不手動指定,那么EF肯定不知道設置哪個為主鍵哪個為外鍵了,這個其實不難理解。按照邏輯Person類的PersonId肯定是主鍵了,直接標注[ForeignKey("PhotoOf")]即可,這是Data Annotation方式配置,自然也可以Fluent API一下,博主個人更喜歡這個方式。 

在演示Fluent API如何配置Person類和PersonPhoto的一對一關系之前,先系統的學習下EF里實體關系配置的方法。EF里的實體關系配置分為Has和With系列的方法:Optional 可選的、Required 必須的、Many 多個。舉例:

A.HasRequired(a => a.B).WithOptional(b => b.A);

這里的a=>a.B是lambda表示寫法,就是找到A類里的導航屬性B。命名a不固定,可以隨意,q=>q.B也是可以的。但是B是A類的屬性,故習慣用小寫a。

Has方法:

  1. HasOptional:前者包含后者一個實例或者為null
  2. HasRequired:前者(A)包含后者(B)一個不為null的實例
  3. HasMany:前者包含后者實例的集合

With方法:

  1. WithOptional:后者(B)可以包含前者(A)一個實例或者null
  2. WithRequired:后者包含前者一個不為null的實例
  3. WithMany:后者包含前者實例的集合

    摘自這里這是較為好的理解方式。上面一句配置意思就是A類包含B類一個不為null的實例,B類包含A類一個實例,也可以不包含。最標准的一對一配置。ok,現在試着寫下上面Person類和PersonPhoto類的一對一的關系如何配置: 

this.HasRequired(p => p.PhotoOf).WithOptional(p => p.Photo);

再跑下程序,數據庫就生成了,是一對一的關系。Person表可以沒有對應的PersonPhoto表數據,但是PersonPhoto表每一條數據都必須對應一條Person表數據。意思就是人可以沒有照片,但是有的照片必須屬於某個人。關系配置是這樣的效果,其實可以隨便改,也可以配置成每個人都必須有對應的照片。把上面的WithOptional改成WithRequired,對應到數據庫里就是null變成了not null。

      思考:這里並沒有像之前一樣添加一個實體類就同時添加到BreakAwayContext類中,但是為何照樣能在數據庫中生成PersonPhotos表?            解答:      添加到BreakAwayContext類中是讓數據庫上下文能跟蹤到這個類,方便進行CRUD(增查改刪)。      這里不把      PersonPhoto       類添加到      BreakAwayContext       類中是因為程序中一般並不會去單獨增刪改查      PersonPhoto              類,對      PersonPhoto              類的操作都是先找Person類,然后通過一對一的關系找到      PersonPhoto              類,這個比較符合實際情況。數據庫中能生成      PersonPhotos       就更好理解了,因為有這個實體類嘛。               思考:如果只需要加入主表類到BreakAwayContext類中,那么其他什么一對多,多對多的關系是不是都只要加主表類到      BreakAwayContext       類中呢?            解答:還是需要根據實際情況考慮,上面的PersonPhoto類已經解釋過了,實際情況中不太可能單獨操作      PersonPhoto       類。一對多關系里Logding住宿類是從表類,Destination是其主表。這個想想也知道必須要讓數據庫上下文跟蹤到      Lodging住宿類      ,因為太可能直接操作Lodging了。比如前台添加一個搜索住宿的功能,那是不是需要直接操作此從表了呢?肯定需要了。所以還是需要根據實際情況考慮。這里僅是個人觀點,如有瑕疵,懇請指正。      

II.實體間一對多的關系       

之前的文章里,景點類Destination和住宿類Lodging是一對多的關系,這個很好理解:一個景點那有多個住宿的地方,而一個住宿的地方只屬於一個景點。當然也可以沒有,一個景點那一個住宿的地方就沒有,一個住宿的地方不屬於任何景點,這個也是可以的。之前的程序實現的就是互相不屬於,全部可空。現在來配置下住宿的地方必須屬於某個景點:

Data Annotations    直接在住宿類Lodging的導航屬性上添加[Required]標注即可: 

[Required]
public Destination Destination { get; set; }

Fluent API

this.HasMany(d => d.Lodgings).WithRequired(l => l.Destination).Map(l => l.MapKey("DestinationId"));

這行是在DestinationMap類里寫的,對應到上面的描述,前者就是Destination,后者是Lodging。整句的意思就是:Destination類包含多個(HasMany)Lodging類實例的集合,Lodging類包含前者一個不為null(WithRequired)的實例。.MapKey是指定外鍵名的。此處如果住宿類不必須屬於某個景點,那么直接把WithRequired換成WithOptional即可。查詢的時候前者使用Inner join,后者使用Left join。不懂Inner、Left和Cross Join區別的點這里 

上面是以Destination為前者的,當然也可以以Lodging為前者,去LodgingMap里寫下如下配置,其實是一個意思:

this.HasRequired(d => d.Destination).WithMany(l => l.Lodgings).Map(l => l.MapKey("DestinationId"));

重跑下程序,生成的數據庫Lodging表的外鍵已經設置成為了不可空,並外鍵名是指定的“DestinationId”:

 官方給出的一對多的解釋是這樣的,其實還沒我解釋的通俗易懂,發個圖你們感受下吧:

ok,上面說了一對多的關系,是標准的一對多關系,兩個表里分別有導航屬性。但是如果有列不遵循這個規則呢?        繼續添加一個新類InternetSpecial,記錄一些跟平常住宿價格不一樣的類,節假日等。這個類不僅有導航屬性Accommodation,還有主鍵列AccommodationId: 

/// <summary> /// 住宿特殊價格類(節假日等) /// </summary> public class InternetSpecial { public int InternetSpecialId { get; set; } public int Nights { get; set; } //幾晚 public decimal CostUSD { get; set; } //價錢 public DateTime FromDate { get; set; } public DateTime ToDate { get; set; } public int AccommodationId { get; set; } public Lodging Accommodation { get; set; } }

同時給住宿類Lodging添加一個    InternetSpecial     類的導航屬性: 

public List<InternetSpecial> InternetSpecials { get; set; }

配置好了跑下程序,生成的數據庫表:

由表可見,不僅有AccommodationId列,還有個外鍵列Accommodation_LodgingId,明顯這個是因為沒有設置外鍵的原因,EF不知道要給哪個屬性當外鍵。現在分別使用Data Annotation和Fluent API設置試試

Data Annotation:

[ForeignKey("Accommodation")] public int AccommodationId { get; set; }

或者這樣:

[ForeignKey("AccommodationId")] public Lodging Accommodation { get; set; }

Fluent API:

this.HasRequired(s => s.Accommodation) .WithMany(l => l.InternetSpecials) .HasForeignKey(s => s.AccommodationId); //外鍵 //如果實體類沒定義AccommodationId,那么可以使用Map方法直接指定外鍵名:.Map(s => s.MapKey("AccommodationId"))

這個就不詳細解釋了,如果還看不懂,看看文章開頭我分析的Has和With系列方法。配置好重新跑下程序,外鍵就是AccommodationId了,沒有多余的Accommodation_LodgingId列了。

III.實體間多對多的關系       

添加一個活動類Activity,跟旅行類Trip是多對多的關系。這個也不難理解:一個旅行有多個活動,一個活動可以屬於多個旅行。

/// <summary> /// 活動類 /// </summary> public class Activity { public int ActivityId { get; set; } //[Required, MaxLength(50)] public string Name { get; set; } public List<Trip> Trips { get; set; } //和Trip類是多對多關系 }

跟之前的一樣在BreakAwayContext類里添加Activity類,讓數據庫上下文知道Activity類:

public DbSet<CodeFirst.Model.Activity> Activitys { get; set; }

同時在Trip旅行類里添加上導航屬性,形成跟Activity活動類的多對多關系

public List<Activity> Activitys { get; set; }

ok,已經可以了,跑下程序得到如下數據庫:

可以看出,EF里的多對多關系是由第三張表來連接兩個表的。ActivityTrips表連接了Activityes表和Trips表。表名列名都是默認命名,都可以自己配置。文章的開頭已經說了那么多了,多對多肯定是用HasMany和WithMany方法,在ActivityMap類里寫下如下Fluent API:

this.HasMany(a => a.Trips).WithMany(t => t.Activitys).Map(m => { m.ToTable("TripActivities"); //中間關系表表名 m.MapLeftKey("ActivityId"); //設置Activity表在中間表主鍵名 m.MapRightKey("TripIdentifier"); //設置Trip表在中間表主鍵名 });

同樣也可以在TripMap里配置,順序不一樣罷了:

this.HasMany(t => t.Activities).WithMany(a => a.Trips).Map(m => { m.ToTable("TripActivities"); //中間關系表表名 m.MapLeftKey("TripIdentifier"); //設置Activity表在中間表的主鍵名 m.MapRightKey("ActivityId"); //設置Trip表在中間表的主鍵名 });

兩種配置任選其一就可以了,重新跑下程序就可以了。都配置好了在程序里如何讀取這個對多對的數據呢,簡單寫一句:

var tripWithActivities = context.Trips.Include("Activities").FirstOrDefault();

很明顯,用到了Include貪婪加載把相關的外鍵表數據(如果有)也拿到了內存中:

    是不是也需要考慮性能的問題呢?如果只需要修改主表的某個列,那貪婪加載出相關聯的從表數據做什么?會發送很多冗余的sql到數據庫。當然如果要根據主表找從表數據的話,這么加載也是好事,超級方便。EF小組的原話是:Entity Framework took care of the joins to get across the join table without you having to be aware of its presence. In the same way, any time you do inserts, updates, or deletes within this many-to-many relationship, Entity Framework will work out the proper SQL for the join without you having to worry about it in your code.    意思就是如果你配置好了主外鍵關系,EF會幫你生成合適的連表查詢(join)sql,不會你再多費心。關於一對多、多對多的EF查詢和效率問題,后續會有專門系列文章講解。 

IV.級聯刪除       

EF配置的外鍵關系除了配置為Optional(可選的,也就是可空),其他默認都是級聯刪除的,意思就是刪除主表的某個數據,相關聯的從表數據都自動刪除:

為了演示添加一個方法:

//級聯刪除(服務端延遲加載) private static void DeleteDestinaInMemoryAndDbCascade() { int destinationId; using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { var destination = new CodeFirst.Model.Destination { Name = "Sample Destination", Lodgings = new List<CodeFirst.Model.Lodging> { new CodeFirst.Model.Lodging {Name="Lodging One"}, new CodeFirst.Model.Lodging {Name="Lodging Two"} } }; context.Destinations.Add(destination); //添加測試數據 context.SaveChanges(); destinationId = destination.DestinationId; //記住主鍵id } using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { //這里用了貪婪加載,把主鍵和相關的外鍵記錄都加載到內存中了 var destination = context.Destinations.Include("Lodgings").Single(d => d.DestinationId == destinationId); var aLodging = destination.Lodgings.FirstOrDefault(); context.Destinations.Remove(destination);  context.SaveChanges(); } }

很簡單,添加了一條主鍵數據Sample Destination,同時添加了以此主鍵為基礎的兩條外鍵數據:Lodging One和Lodging Two,即:添加了一個旅游景點,又添加了此旅游景點下的兩個住宿的地方。之后延遲加載出主表數據和相關聯的兩條從表數據並刪除,使用sql profiler能監測到如下sql:

第一條是刪除主表的數據,后兩條是刪除相關聯從表數據的sql。這種級聯刪除稍顯麻煩,同時加載了相關聯從表的數據到內存中再發送刪除命令到數據庫。其實只需要加載要刪除的主表記錄到內存中就可以了,因為數據庫已經打開了級聯刪除,只需要發送刪除主表數據的指令到數據庫,數據庫會自動刪除相關聯的從表記錄。可以監控到如下sql:

    
exec sp_executesql N'SELECT [Project2].[DestinationId] AS [DestinationId], [Project2].[Name] AS [Name], [Project2].[Country] AS [Country], [Project2].[Description] AS [Description], [Project2].[image] AS [image], [Project2].[C1] AS [C1], [Project2].[LodgingId] AS [LodgingId], [Project2].[Name1] AS [Name1], [Project2].[Owner] AS [Owner], [Project2].[IsResort] AS [IsResort], [Project2].[MilesFromNearestAirport] AS [MilesFromNearestAirport], [Project2].[PrimaryContact_PersonId] AS [PrimaryContact_PersonId], [Project2].[SecondaryContact_PersonId] AS [SecondaryContact_PersonId], [Project2].[DestinationId1] AS [DestinationId1] FROM ( SELECT [Limit1].[DestinationId] AS [DestinationId], [Limit1].[Name] AS [Name], [Limit1].[Country] AS [Country], [Limit1].[Description] AS [Description], [Limit1].[image] AS [image], [Extent2].[LodgingId] AS [LodgingId], [Extent2].[Name] AS [Name1], [Extent2].[Owner] AS [Owner], [Extent2].[IsResort] AS [IsResort], [Extent2].[MilesFromNearestAirport] AS [MilesFromNearestAirport], [Extent2].[PrimaryContact_PersonId] AS [PrimaryContact_PersonId], [Extent2].[SecondaryContact_PersonId] AS [SecondaryContact_PersonId], [Extent2].[DestinationId] AS [DestinationId1], CASE WHEN ([Extent2].[LodgingId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1] FROM (SELECT TOP (2) [Extent1].[DestinationId] AS [DestinationId], [Extent1].[Name] AS [Name], [Extent1].[Country] AS [Country], [Extent1].[Description] AS [Description], [Extent1].[image] AS [image] FROM [dbo].[Destinations] AS [Extent1] WHERE [Extent1].[DestinationId] = @p__linq__0 ) AS [Limit1] LEFT OUTER JOIN [dbo].[Lodgings] AS [Extent2] ON [Limit1].[DestinationId] = [Extent2].[DestinationId] ) AS [Project2] ORDER BY [Project2].[DestinationId] ASC, [Project2].[C1] ASC',N'@p__linq__0 int',@p__linq__0=3
View Code  

直接復制到數據庫執行查詢,發現它會返回一條主表數據和兩條相關聯的從表數據。除非必須查出外鍵記錄才使用Include貪婪加載,否則千萬不要,EF中跟手寫ado不一樣,很容易生成很冗余的sql。這里其實只需要主鍵的記錄就可以了,修改下方法:

    //級聯刪除(僅加載主鍵記錄) private static void DeleteDestinationInMemeryAndDbCascade() { int destinationId; using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { var destination = new CodeFirst.Model.Destination { Name = "Sample Destination", Lodgings = new List<CodeFirst.Model.Lodging> { new CodeFirst.Model.Lodging {Name="Lodging One"}, new CodeFirst.Model.Lodging {Name="Lodging Two"} } }; context.Destinations.Add(destination); context.SaveChanges(); destinationId = destination.DestinationId; } using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { var destination = context.Destinations .Single(d => d.DestinationId == destinationId); //只取一條主鍵記錄  context.Destinations.Remove(destination); //然后移除主鍵記錄,外鍵記錄又數據庫級聯刪除 context.SaveChanges(); } }

監控的sql干干凈凈,只會查出主表數據。

exec sp_executesql N'SELECT TOP (2) [Extent1].[DestinationId] AS [DestinationId], [Extent1].[Name] AS [Name], [Extent1].[Country] AS [Country], [Extent1].[Description] AS [Description], [Extent1].[image] AS [image] FROM [dbo].[Destinations] AS [Extent1] WHERE [Extent1].[DestinationId] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=1

補充:這里只查一條記錄卻使用SELECT TOP (2)... 是保證能查到記錄。

刪除sql更干凈,只刪除主表數據,相關聯的從表數據刪除由數據庫級聯刪除完成:

exec sp_executesql N'delete [dbo].[Destinations] where ([DestinationId] = @0)',N'@0 int',@0=1

級聯刪除雖然方便,但是並不常用。試想我們在博客園寫了很多隨筆,為不同隨筆加了不同的標簽好區分和管理。某一天突然發現之前定的某個標簽並不合理,但是這個標簽已經在很多隨筆里用了,如果此時刪除標簽,數據庫級聯的把標注此標簽的隨筆都刪了,這個肯定不合適。應該是標簽刪了,之前貼過此標簽的文章沒了這個標簽,這個才符合邏輯。

數據庫里可以可視化的設置不級聯刪除,Fluent API配置此外鍵關系時可以設置不級聯刪除:

this.HasMany(d => d.Lodgings).WithRequired(l => l.Destination) .Map(l => l.MapKey("DestinationId")) //一對多並指定外鍵名 .WillCascadeOnDelete(false); // 關閉級聯刪除

再跑下程序,去看下數據庫本外鍵自然就沒了級聯刪除。

    園友郭明鋒提供了一個很好的建議:考慮到EF中的級聯刪除並不常用,所以可以在全局里關掉所有主外鍵關系的級聯刪除,如果需要可以打開某個主外鍵的級聯刪除。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM