本文出自8天掌握EF的Code First開發系列,通過本身的實踐整理出來。html
本篇目錄web
本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。sql
上一篇《Code First開發系列之領域建模和管理實體關係》,咱們主要介紹了EF中「約定大於配置」的概念,如何建立數據庫的表結構,以及如何管理實體間的三種關係和三種繼承模式。這一篇咱們主要說三方面的問題,數據庫建立的管理,種子數據的填充以及CRUD的操做詳細用法。數據庫
管理數據庫建立swift
一、管理數據庫鏈接c#
(1) 使用配置文件管理鏈接windows
在數據庫上下文類中,若是咱們只繼承了無參數的DbContext,而且在配置文件中建立了和數據庫上下文類同名的鏈接字符串,那麼EF會使用該鏈接字符串自動計算出該數據庫的位置和數據庫名。好比,咱們的上下文定義以下:服務器
public class SampleDbEntities : DbContext { }
若是咱們在配置文件中定義的鏈接字符串以下:markdown
<add name="SampleDbEntities" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\myTestDb.mdf" providerName="System.Data.SqlClient" />
這樣,EF會使用該鏈接字符串執行數據庫操做。究竟發生了什麼呢?咱們經過示例代碼來驗證一下。ide
public class SampleDbEntities : DbContext { public DbSet<Student> Students { get; set; } } public class Student { public int Id { get; set; } public string Name { get; set; } }
class Program { static void Main(string[] args) { using (var context = new SampleDbEntities()) { var stu1 = new Student() {Name = "Paul Huang"}; context.Students.Add(stu1); context.SaveChanges(); } Console.WriteLine("Finished"); Console.ReadKey(); } }
當運行應用程序時,可能會出現「Error: A file activation error occurred. CREATE DATABASE failed.」的異常,解決方案請參考,示例代碼以下代碼:
public class SampleDbEntities : DbContext { public SampleDbEntities() { AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); } public DbSet<Student> Students { get; set; } }
當運行應用程序時,EF會尋找咱們的上下文類名,即「SampleDbEntities」,並在配置文件中尋找和它同名的鏈接字符串,而後它會使用該鏈接字符串計算出應該使用哪一個數據庫provider,以後檢查數據庫位置(例子中是當前的數據目錄),以後會在指定的位置建立一個名爲myTestDb.mdf的數據庫文件,同時根據鏈接字符串的Initial Catalog屬性建立了一個名爲myTestDb的數據庫。
使用配置文件指定數據庫位置和名字對於控制上下文類的鏈接參數也許是最簡單和最有效的方式,另外一個好處是若是咱們想爲開發,生產和臨時環境建立各自的鏈接字符串,那麼在配置文件中更改鏈接字符串並在開發時將它指向肯定的數據庫也是一種方法。
(2) 使用已存在的ConnectionString
若是咱們已經有了一個定義數據庫位置和名稱的ConnectionString,而且咱們想在數據庫上下文類中使用這個鏈接字符串,以下:
<add name="AppConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\myTestDb2.mdf" providerName="System.Data.SqlClient" />
那麼咱們能夠將該鏈接字符串的名字傳入數據庫上下文DbContext的構造函數中,以下所示:
public SampleDbEntities() : base("name = AppConnection") { AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); }
上面的代碼將鏈接字符串的名字傳給了DbContext類的構造函數,這樣一來,咱們的數據庫上下文就會開始使用鏈接字符串了。可是注意有一個問題,若是使用「(1) 使用配置文件管理鏈接」已經在Bin/Debug下面生成了myTestDb.mdf文件,再運行"(2) 使用已存在的ConnectionString"時就會出現以下錯誤:
解決方法以下:
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new DropCreateDatabaseAlways<SampleDbEntities>()); AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Directory.GetCurrentDirectory()); } public DbSet<Student> Students { get; set; } }
若是在配置文件中還有一個和數據庫上下文類名同名的connectionString,也不會使用這個同名的鏈接字符串,也就是說顯式指定的鏈接字符串優先權更大。
(3) 使用已存在的鏈接
一般在一些老項目中,咱們只會在項目中的某個部分使用EF Code First,同時,咱們想對數據上下文類使用已經存在的數據庫鏈接,若是要實現這個,可將鏈接對象傳給DbContext類的構造函數,以下:
public SampleDbEntities(DbConnection dbConnection) : base(dbConnection, false) { }
這裏要注意一下contextOwnsConnection
參數,之因此將它做爲false傳入到上下文,是由於它是從外部傳入的,當上下文出了範圍時,可能會有人想要使用該鏈接。若是傳入true的話,那麼一旦上下文出了範圍,數據庫鏈接就會當即關閉。
二、管理數據庫初始化
首次運行EF Code First應用時,EF會作下面的這些事情:
DbContext
類。connectionString
。一旦模式信息提取出來,EF會使用數據庫初始化器將該模式信息推送給數據庫。數據庫初始化器有不少可能的策略,EF默認的策略是若是數據庫不存在,那麼就從新建立;若是存在的話就使用當前存在的數據庫。固然,咱們有時也可能須要覆蓋默認的策略,可能用到的數據庫初始化策略以下:
CreateDatabaseIfNotExists
:顧名思義,若是數據庫不存在,那麼就從新建立,不然就使用現有的數據庫。若是從領域模型中提取到的模式信息和實際的數據庫模式不匹配,那麼就會拋出異常。DropCreateDatabaseAlways
:若是使用了該策略,那麼每次運行程序時,數據庫都會被銷燬。這在開發週期的早期階段一般頗有用(好比設計領域實體時),從單元測試的角度也頗有用。DropCreateDatabaseIfModelChanges
:這個策略的意思就是說,若是領域模型發生了變化(具體而言,從領域實體提取出來的模式信息和實際的數據庫模式信息失配時),就會銷燬之前的數據庫(若是存在的話),並建立新的數據庫。MigrateDatabaseToLatestVersion
:若是使用了該初始化器,那麼不管何時更新實體模型,EF都會自動地更新數據庫模式。這裏很重要的一點是,這種策略更新數據庫模式不會丟失數據,或者是在已有的數據庫中更新已存在的數據庫對象。(1) 設置初始化策略
EF默認使用CreateDatabaseIfNotExists
做爲默認初始化器,若是要覆蓋這個策略,那麼須要在DbContext類中的構造函數中使用Database.SetInitializer
方法,下面的例子使用DropCreateDatabaseIfModelChanges
策略覆蓋默認的策略:
public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SampleDbEntities>()); }
這樣一來,不管何時建立上下文類,Database.SetInitializer
方法都會被調用,而且將數據庫初始化策略設置爲DropCreateDatabaseIfModelChanges
。
若是處於生產環境,那麼咱們確定不想丟失已存在的數據。這時咱們就須要關閉該初始化器,只須要將null
傳給Database.SetInitializer
方法,以下所示:
public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer<SampleDbEntities>(null); }
填充種子數據
到目前爲止,不管咱們選擇哪一種策略初始化數據庫,生成的數據庫都是一個空的數據庫。可是許多狀況下咱們總想在數據庫建立以後、首次使用以前就插入一些數據,此外,開發階段可能想以admin的資格爲其填充一些數據,或者爲了測試應用在特定的場景中表現如何,想要僞造一些數據。
當咱們使用 DropCreateDatabaseAlways
和 DropCreateDatabaseIfModelChanges
初始化策略時,插入種子數據很是重要,由於每次運行應用時,數據庫都要從新建立,每次數據庫建立以後再手動插入數據很是乏味。接下來咱們看一下當數據庫建立以後如何使用EF來插入種子數據。
爲了向數據庫插入一些初始化數據,咱們須要建立知足下列條件的數據庫初始化器類:
一、定義領域實體
假設咱們的數據模型Employer定義以下:
public class Employer { public int Id { get; set; } public string EmployerName { get; set; } }
二、建立數據庫上下文
使用EF的Code First方法對上面的模型建立數據庫上下文:
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { } public DbSet<Employer> Employers { get; set; } }
三、建立數據庫初始化器類
假設咱們使用的是 DropCreateDatabaseAlways
數據庫初始化策略,那麼初始化器類就要從該泛型類繼承,並傳入數據庫上下文做爲類型參數。接下來,要種子化數據庫就要重寫DropCreateDatabaseAlways
類的Seed
方法,而Seed方法拿到了數據庫上下文,所以咱們可使用它來將數據插入數據庫:
public class SeedingDataInitializer : DropCreateDatabaseAlways<SampleDbEntities> { protected override void Seed(SampleDbEntities context) { for (int i = 0; i < 6; i++) { var employer = new Employer { EmployerName = "Employer" + (i + 1) }; context.Employers.Add(employer); } base.Seed(context); } }
前面的代碼經過for循環建立了6個Employer對象,並將它們添加給數據庫上下文類的Employers
集合屬性。這裏值得注意的是咱們並無調用DbContext.SaveChanges()
,由於它會在基類中自動調用。
四、將數據庫初始化器類用於數據庫上下文類
public class SampleDbEntities : DbContext { public SampleDbEntities() : base("name = AppConnection") { Database.SetInitializer(new SeedingDataInitializer()); } public DbSet<Employer> Employers { get; set; } }
五、Main方法中訪問數據庫
static void Main(string[] args) { using (var db = new SampleDbEntities()) { var employers = db.Employers; foreach (var employer in employers) { Console.WriteLine("Id={0}\tName={1}", employer.Id, employer.EmployerName); } } Console.WriteLine("DB建立成功,並完成種子化!"); Console.Read(); }
六、運行程序,查看效果
Main方法中只是簡單的建立了數據庫上下文對象,而後將數據讀取出來:
此外,咱們能夠從數據庫初始化的Seed
方法中,經過數據庫上下文類給數據庫傳入原生SQL來影響數據庫模式。
LINQ to Entities詳解
到目前爲止,咱們已經學會了如何使用Code First方式來建立實體數據模型,也學會了使用EF進行領域建模,執行模型驗證以及控制數據庫鏈接參數。一旦數據建模完成,接下來就是要對這些模型進行各類操做了,一般有如下兩種方式:
本系列教程只講LINQ to Entities,Entity SQL就是經過在EF中執行SQL,你們能夠自行研究。
一、什麼是LINQ to Entities
LINQ,全稱是Language-INtegrated Query(集成語言查詢),是.NET語言中查詢數據的一種技術。LINQ to Entities 是一種機制,它促進了使用LINQ對概念模型的查詢。
由於LINQ是聲明式語言,它讓咱們聚焦於咱們須要什麼數據而不是應該如何檢索數據。LINQ to Entities在實體數據模型之上提供了一個很好的抽象,因此咱們可使用LINQ來指定檢索什麼數據,而後LINQ to Entities provider會處理訪問數據庫事宜,併爲咱們取到必要的數據。
當咱們使用LINQ to Entities對實體數據模型執行LINQ查詢時,這些LINQ查詢會首先被編譯以決定咱們須要獲取什麼數據,而後執行編譯後的語句,從應用程序的角度看,最終會返回.NET理解的CLR對象。
上圖展現了LINQ to Entities依賴EntityClient
纔可以使用EF的概念數據模型,接下來咱們看下LINQ to SQL如何執行該查詢並給應用程序返回結果:
EntityClient
命令。EntityClient
命令而後使用EF和實體數據模型將這些命令轉換成SQL查詢。EntityClient
使用項目,並返回必要的結果給應用程序。
EntityClient
對象寄居在System.Data.EntityClient
命名空間中,咱們沒必要顯式建立該對象,咱們只須要使用命名空間,而後LINQ to Entities會處理剩下的事情。
若是咱們對多種類型的數據庫使用LINQ to Entities,那麼咱們只須要爲該數據庫使用正確的ADO.NET provider,而後EntityClient
就會使用這個provider對任何數據庫的LINQ查詢無縫執行。
二、使用LINQ to Entities操做實體
編寫LINQ查詢的方式有兩種:
選擇哪一種語法徹底取決你的習慣,兩種語法的性能是同樣的。查詢語法相對更容易理解,可是靈活性稍差;相反,方法語法理解起來有點困難,可是提供了更強大的靈活性。使用方法語法能夠進行連接多個查詢,所以在單個語句中能夠實現最大的結果。
下面以一個簡單的例子來理解一下這兩種方法的區別。建立一個控制檯應用,名稱爲「Donators_CRUD_Demo」,該demo也用於下面的CRUD一節。
領域實體模型定義以下:
public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } }
數據庫上下文定義以下:
public class DonatorsContext : DbContext { public DonatorsContext() : base("name=EFCodeFirst") { } public virtual DbSet<Donator> Donators { get; set; } }
定義好鏈接字符串以後,若是使用該實體數據模型經過執行LINQ查詢來獲取Donator數據,那麼能夠在數據庫上下文類的Donators集合上操做。下面咱們用兩種方法來實現「找出打賞了50元的打賞者」。
(1) 查詢語法
//1.查詢語法 var donators = from donator in db.Donators where donator.Amount == 50 select donator;
(2) 方法語法
//2.方法語法 var donators = db.Donators.Where(d => d.Amount == 50m);
完整的Main方法以下:
static void Main(string[] args) { using (var db = new DonatorsContext()) { //1.查詢語法 //var donators = from donator in db.Donators where donator.Amount == 50 select donator; //2.方法語法 var donators = db.Donators.Where(d => d.Amount == 50m); Console.WriteLine("Id\t姓名\t金額\t打賞日期"); foreach (var donator in donators) { Console.WriteLine("{0}\t{1}\t{2}\t{3}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } } Console.WriteLine("Operation completed!"); Console.Read(); }
兩種方法的LINQ查詢咱們都是使用了var
隱式類型變量將LINQ查詢的結果存儲在了donators變量中。使用LINQ to Entities,咱們可使用隱式類型變量做爲輸出結果,編譯器能夠由該隱式變量基於LINQ查詢推斷出輸出類型。通常而言,輸出類型是IQueryable<T>
類型,咱們的例子中應該是IQueryable<Donator>
。固然咱們也能夠明確指定返回的類型爲IQueryable<Donator>
或者IEnumerable<Donator>
。
重點理解
當使用LINQ to Entities時,理解什麼時候使用
IEnumerable
和IQueryable
很重要。若是使用了IEnumerable
,查詢會當即執行,若是使用了IQueryable
,直到應用程序請求查詢結果的枚舉時纔會執行查詢,也就是查詢延遲執行了,延遲到的時間點是枚舉查詢結果時。
如何決定使用IEnumerable
仍是IQueryable
呢?使用IQueryable
會讓你有機會建立一個使用多條語句的複雜LINQ查詢,而不須要每條查詢語句都對數據庫執行查詢。該查詢只有在最終的LINQ查詢要求枚舉時纔會執行。
三、LINQ操做
爲了方便展現,咱們須要再建立一張表,所以,咱們須要再定義一個實體類,而且要修改以前的實體類,以下所示:
public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } public virtual Province Province { get; set; } }
public class Province { public Province() { Donators = new Collection<Donator>(); } public int Id { get; set; } [StringLength(225)] public string ProvinceName { get; set; } public virtual ICollection<Donator> Donators { get; set; } }
從上面定義的POCO類,咱們不難發現,這兩個實體之間是一對多的關係,一個省份可能會有多個打賞者,至於爲什麼這麼定義,上一篇已經提到了,這篇再也不囉嗦。Main方法添加了一句代碼Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DonatorsContext>());
,運行程序,會生成新的數據庫,而後插入如下數據(數據純粹是爲了演示,不具真實性):
INSERT dbo.Provinces VALUES( N'山東省') INSERT dbo.Provinces VALUES( N'河北省') INSERT dbo.Donators VALUES ( N'陳志康', 50, '2016-04-07',1) INSERT dbo.Donators VALUES ( N'海風', 5, '2016-04-08',1) INSERT dbo.Donators VALUES ( N'醉、千秋', 12, '2016-04-13',1) INSERT dbo.Donators VALUES ( N'雪茄', 18.8, '2016-04-15',2) INSERT dbo.Donators VALUES ( N'王小乙', 10, '2016-04-09',2)
(1) 執行簡單的查詢
平時咱們會常常須要從某張表中查詢全部數據的集合,如這裏查詢全部打賞者的集合:
//查詢語法 //var donators = from donator in context.Donators select donator; //方法語法 var donators = context.Donators;
下面是該LINQ查詢生成的SQL:
SELECT [t0].[Id], [t0].[Name], [t0].[Amount], [t0].[DonateDate], [t0].[Province_Id]
FROM [Donators] AS [t0]
LINQPad是一款練習LINQ to Entities出色的工具。在LINQPad中,咱們已經在DbContext或ObjectContext內部了,不須要再實例化數據庫上下文了,咱們可使用LINQ to Entities查詢數據庫。咱們也可使用LINQPad查看生成的SQL查詢了。
LINQPad多餘的就不介紹了,看下圖,點擊圖片下載並學習。
下圖爲LINQPad將linq語法轉換成了SQL。
(2) 使用導航屬性
若是實體間存在一種關係,那麼這個關係是經過它們各自實體的導航屬性進行暴露的。在上面的例子中,省份Province
實體有一個Donators集合屬性用於返回該省份的全部打賞者,而在打賞者Donator
實體中,也有一個Province屬性用於跟蹤該打賞者屬於哪一個省份。導航屬性簡化了從一個實體到和它相關的實體,下面咱們看一下如何使用導航屬性獲取與其相關的實體數據。
好比,咱們想要獲取「山東省的全部打賞者」:
//查詢語法 var donators = from province in context.Provinces where province.ProvinceName == "山東省" from donator in province.Donators select donator; //查詢語法 var donators = context.Provinces.Where(province => province.ProvinceName == "山東省").SelectMany(province => province.Donators);
最終的查詢結果都是同樣的:
反過來,若是咱們想要獲取打賞者「雪茄」的省份:
//查詢語法 var provices = from donator in context.Donators where donator.Name == "雪茄" select donator.Province; //方法語法 //var provices = context.Donators.Where(donator => donator.Name == "雪茄").Select(donator => donator.Province);
(3) 過濾數據
實際上以前已經介紹了,根據某些條件過濾數據,能夠在LINQ查詢中使用Where
。好比上面咱們查詢了山東省的全部打賞者,這裏咱們過濾出打賞金額在10~20元之間的打賞者:
//查詢語法 /* var donators = from donator in context.Donators where donator.Amount >= 10M && donator.Amount <= 20M select donator; */ //方法語法 var donators = context.Donators.Where(donator => donator.Amount >= 10 && donator.Amount <= 20);
最終查詢的結果以下:
生成的SQL語句在這裏不在貼出來了,你們本身經過LINQPad或者其餘工具本身去看吧!只要知道EF會幫助咱們自動將LINQ查詢轉換成合適的SQL語句就能夠了。
(4) LINQ投影
若是不指定投影的話,那麼默認就是選擇該實體或與之相關實體的全部字段,LINQ投影就是返回這些實體屬性的子集或者返回一個包含了多個實體的某些屬性的對象。
投影通常用在應用程序中的VIewModel(視圖模型),咱們能夠從LINQ查詢中直接返回一個視圖模型。好比,咱們想要查出「全部省的全部打賞者」:
class Program { static void Main(string[] args) { using (var context = new DonatorsContext()) { //查詢語法 /* var provinces = from province in context.Provinces select new { Province = province, Donators = province.Donators }; */ //方法語法 var provinces = context.Provinces.Select(province => new { Province = province, Donators = province.Donators }); foreach (var province in provinces) { foreach (var donator in province.Donators) { Console.WriteLine("{0}\t{1}", province.Province.ProvinceName, donator.Name); } } } Console.WriteLine("Operation completed!"); Console.Read(); } }
執行結果以下:
固然,若是咱們已經定義了一個包含了Province
和DonatorList
屬性的類型(好比視圖模型),那麼也能夠直接返回該類型,下面只給出方法語法(查詢語法你們可自行寫出)的寫法:
public class DonatorsWithProvinceViewModel { public string Province { get; set; } public ICollection<Donator> DonatorList { get; set; } }
//方法語法 var provinces = context.Provinces.Select(province => new DonatorsWithProvinceViewModel() { Province = province.ProvinceName, DonatorList = province.Donators });
在
IQueryable<T>
中處理結果也會提高性能,由於直到要查詢的結果進行枚舉時纔會執行生成的SQL。
(5) 分組Group
分組的重要性相必你們都知道,這個確定是要掌握的!下面就看看兩種方法的寫法。
//查詢語法 /* var donatorsWithProvince = from donator in context.Donators group donator by donator.Province.ProvinceName into groupedProvince select new { ProvinceName = groupedProvince.Key, Donators = groupedProvince }; */ //方法語法 var donatorsWithProvince = context.Donators.GroupBy(donator => donator.Province.ProvinceName).Select(groupedDonators => new { ProvinceName = groupedDonators.Key, Donators = groupedDonators }); foreach (var donatorWithProvince in donatorsWithProvince) { Console.WriteLine("{0}\t{1}", donatorWithProvince.ProvinceName, donatorWithProvince.Donators.Count()); }
稍微解釋一下吧,上面的代碼會根據省份名稱進行分組,最終以匿名對象的投影返回。結果中的ProvinceName就是分組時用到的字段,Donators屬性包含了經過ProvinceName找到的Donator集合。
執行結果以下:
(6) 排序Ordering
對特定的列進行升序或降序排列也是常用的操做。好比咱們按照打賞金額升序排序。
//查詢語法 var orderedDonators = from donator in context.Donators orderby donator.Amount descending select donator; //方法語法 //var orderedDonators = context.Donators.OrderByDescending(item => item.Amount); foreach (var orderedDonator in orderedDonators) { Console.WriteLine("{0}\t{1}", orderedDonator.Name, orderedDonator.Amount); }
升序查詢執行結果:
只要刪除掉descending關鍵字就是升序排序了,默認排序方式是升序。
(7) 聚合操做
使用LINQ to Entities能夠執行下面的聚合操做:
下面我找出山東省打賞者的數量:
using (var context = new DonatorsContext()) { //查詢語法 var count1 = (from donator in context.Donators where donator.Province.ProvinceName == "山東省" select donator).Count(); //方法語法 var count2 = context.Donators.Count(item => item.Province.ProvinceName == "山東省"); Console.WriteLine(count1); Console.WriteLine(count2); }
執行結果見下圖,可見,方法語法更加簡潔,並且查詢語法還要將前面的LINQ sql用括號括起來才能進行聚合(其實這是混合語法),沒有方法語法簡單靈活,因此下面的幾個方法咱們只用方法語法進行演示。
其餘聚合函數的代碼:
using (var context = new DonatorsContext()) { var sum = context.Donators.Sum(d => d.Amount);//計算全部打賞者的金額總和 var min = context.Donators.Min(d => d.Amount);//最少的打賞金額 var max = context.Donators.Max(d => d.Amount);//最多的打賞金額 var average = context.Donators.Average(d => d.Amount);//打賞金額的平均值 Console.WriteLine("Sum={0},Min={1},Max={2},Average={3}", sum, min, max, average); }
執行結果:
(8)分頁Paging
分頁也是提高性能的一種方式,而不是將全部符合條件的數據一次性所有加載出來。在LINQ to Entities中,實現分頁的兩個主要方法是:Skip
和Take
,這兩個方法在使用前都要先進行排序,切記。
Skip
該方法用於從查詢結果中跳過前N條數據。假如咱們根據Id排序後,跳過前2條數據:
static void Main(string[] args) { using (var context = new DonatorsContext()) { var donatorsBefore = context.Donators; var donatorsAfter = context.Donators.OrderBy(d => d.Id).Skip(2); Console.WriteLine("原始數據打印結果:"); PrintDonators(donatorsBefore); Console.WriteLine("Skip(2)以後的結果:"); PrintDonators(donatorsAfter); } Console.Read(); } static void PrintDonators(IQueryable<Donator> donators) { Console.WriteLine("Id\t\t姓名\t\t金額\t\t打賞日期"); foreach (var donator in donators) { Console.WriteLine("{0,-10}\t{1,-10}\t{2,-10}\t{3,-10}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } }
執行結果以下:
Take
Take方法用於從查詢結果中限制元素的數量。好比咱們只想取出前3條打賞者:
context.Donators.OrderBy(d => d.Id).Take(3);
分頁實現
若是咱們要實現分頁功能,那麼咱們必須在相同的查詢中同時使用Skip和Take方法。
因爲如今我數據庫只有5條打賞者的數據,因此我打算每頁2條數據,這樣就會有3頁數據。
static void Main(string[] args) { using (var context = new DonatorsContext()) { while (true) { Console.WriteLine("您要看第幾頁數據"); string pageStr = Console.ReadLine() ?? "1"; int page = int.Parse(pageStr); const int pageSize = 2; if (page > 0 && page < 4) { var donators = context.Donators.OrderBy(d => d.Id).Skip((page - 1) * pageSize).Take(pageSize); PrintDonators(donators); } else { break; } } } Console.Read(); }
執行結果以下:
和聚合函數同樣,分頁操做只有方法語法。
(9) 實現多表鏈接join
若是兩個實體之間是互相關聯的,那麼EF會在實體中建立一個導航屬性來訪問相關的實體。也可能存在一種狀況,兩個實體之間有公用的屬性,可是沒有在數據庫中定義它們間的關係。若是咱們要使用該隱式的關係,那麼能夠鏈接相關的實體。
可是以前咱們建立實體類時已經給兩個實體創建了一對多關係,因此這裏咱們使用導航屬性模擬join鏈接:
var join1 = from province in context.Provinces join donator in context.Donators on province.Id equals donator.Province.Id into donators select new { ProvinceName = province.ProvinceName, Donators = donators }; var join2 = context.Provinces.GroupJoin(context.Donators, province => province.Id, donator => donator.Province.Id, (provice, donatorList) => new { ProvinceName = provice.ProvinceName, Donators = donatorList });
LINQ中的
join
和GroupJoin
至關於SQL中的Left Outer Join
。不管右邊實體集合中是否包含任何實體,它老是會返回左邊集合的全部元素。
四、懶加載和預加載
使用LINQ to Entities時,理解懶加載和預加載的概念很重要。由於理解了這些,就會很好地幫助你編寫有效的LINQ查詢。
(1) 懶加載
懶加載是這樣一種過程,直到LINQ查詢的結果被枚舉時,該查詢涉及到的相關實體纔會從數據庫加載。若是加載的實體包含了其餘實體的導航屬性,那麼直到用戶訪問該導航屬性時,這些相關的實體纔會被加載。
在咱們的領域模型中,Donator類的定義以下:
public class Donator { public int Id { get; set; } public string Name { get; set; } public decimal Amount { get; set; } public DateTime DonateDate { get; set; } public virtual Province Province { get; set; } }
當咱們使用下面的代碼查詢數據時,實際上並無從數據庫中加載數據:
var donators = context.Donators;
要真正從數據庫中加載數據,咱們要枚舉donators
,經過ToList()方法或者在foreach循環中遍歷均可以。
看下面的代碼解釋:
//尚未查詢數據庫 var donators = context.Donators; //已經查詢了數據庫,但因爲懶加載的存在,尚未加載Provinces表的數據 var donatorList = donators.ToList(); //由於用戶訪問了Province表的數據,所以這時才加載 var province = donatorList.ElementAt(0).Province;
使用Code First時,懶加載依賴於導航屬性的本質。若是導航屬性是virtual
修飾的,那麼懶加載就開啓了,若是要關閉懶加載,不要給導航屬性加virtual
關鍵字就能夠了。
若是想要爲全部的實體關閉懶加載,那麼能夠在數據庫中的上下文中去掉實體集合屬性的virtual
關鍵字便可。
預加載是這樣一種過程,當咱們要加載查詢中的主要實體時,同時也加載與之相關的實體。要實現預加載,咱們要使用Include
方法。下面咱們看一下如何在加載Donator數據的時候,同時也預先加載全部的Provinces數據:
var donators2 = context.Donators.Include(d => d.Province).ToList(); var donators3 = context.Donators.Include("Provinces").ToList();
這樣,當咱們從數據庫中取到Donators集合時,也取到了Provinces集合。
五、插入數據
將新的數據插入數據庫有多種方法,可使用以前的Add
方法,也能夠給每一個實體的狀態設置爲Added
。若是你要添加的實體包含子實體,那麼Added
狀態會擴散到該圖的全部對象中。換言之,若是根實體是新的,那麼EF會假定你附加了一個新的對象圖。該對象圖通常指的是許多相關的實體造成的一個複雜的樹結構。好比,好比咱們有一個Province對象,每一個省份有不少打賞者Donators,包含在Province類的List屬性中,那麼咱們就是在處理一個對象圖,本質上,Donator
實體是person對象的孩子。
首先,咱們建立一個新的具備打賞者的Province實例,而後,咱們把該實例添加到數據庫上下文中,最後,調用SaveChanges
將數據行提交到數據庫:
var province = new Province { ProvinceName = "浙江省" }; province.Donators.Add(new Donator { Name = "星空夜焰", Amount = 50m, DonateDate = DateTime.Parse("2016-5-30") }); province.Donators.Add(new Donator { Name = "偉濤", Amount = 25m, DonateDate = DateTime.Parse("2016-5-25") }); using (var db = new DonatorsContext()) { db.Provinces.Add(province); db.SaveChanges(); }
這和以前看到的代碼仍是有些不一樣的。咱們在初始化上下文以前就建立了對象,這個代表了EF會追蹤當時上下文中爲attached或者added狀態的實體。
另外一種插入新數據的方法是使用DbContext
API直接設置實體的狀態,例如:
var province2 = new Province { ProvinceName = "廣東省" }; province2.Donators.Add(new Donator { Name = "邱宇", Amount = 30, DonateDate = DateTime.Parse("2016-04-25") }); using (var db = new DonatorsContext()) { db.Entry(province2).State = EntityState.Added; db.SaveChanges(); }
DbContext
上的Entry
方法返回了一個DbEntityEntry
類的實例。該類有許多有用的屬性和方法用於EF的高級實現和場景。下面是EntityState
的枚舉值:
狀態 | 描述 |
---|---|
Added | 添加了一個新的實體。該狀態會致使一個插入操做。 |
Deleted | 將一個實體標記爲刪除。設置該狀態時,該實體會從DbSet中移除。該狀態會致使刪除操做。 |
Detached | DbContext再也不追蹤該實體。 |
Modified | 自從DbContext開始追蹤該實體,該實體的一個或多個屬性已經更改了。該狀態會致使更新操做。 |
Unchanged | 自從DbContext開始追蹤該實體以來,它的任何屬性都沒有改變。 |
執行結果以下,可見剛纔添加的數據都插入數據庫了。
六、更新數據
當EF知道自從實體首次附加到DbContext以後發生了改變,那麼就會觸發一個更新查詢。自從查詢數據時起,EF就會開始追蹤每一個屬性的改變,當最終調用SaveChanges
時,只有改變的屬性會包括在更新SQL操做中。當想要在數據庫中找到一個要更新的實體時,咱們可使用where方法來實現,也可使用DbSet上的Find
方法,該方法須要一個或多個參數,該參數對應於表中的主鍵。下面的例子中,咱們使用擁有惟一ID的列做爲主鍵,所以咱們只須要傳一個參數。若是你使用了複合主鍵(包含了不止一列,常見於鏈接表),就須要傳入每列的值,而且主鍵列的順序要準確。
var donator = context.Donators.Find(3); donator.Name = "醉千秋";//我想把「醉、千秋」中的頓號去掉 context.SaveChanges();
若是執行了SaveChanges以後,你跟蹤發送到SQL Server數據庫的SQL查詢時,會發現執行了下面的sql語句:
UPDATE [dbo].[Donators] SET [Name] = @0 WHERE ([Id] = @1)
這個sql查詢確實證實了只有那些顯式修改的更改纔會發送給數據庫。好比咱們只更改了Donator的Name屬性,其餘都沒動過,生成的sql也是隻更新Name字段。若是在SQL Profiler中查看整個代碼塊,會發現Find方法會生成下面的SQL代碼:
SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Amount] AS [Amount], [Extent1].[DonateDate] AS [DonateDate], [Extent1].[Province_Id] AS [Province_Id] FROM [dbo].[Donators] AS [Extent1] WHERE [Extent1].[Id] = @p0
Find方法被翻譯成了SingleOrDefault方法,因此是Select Top(2)。若是你在寫桌面應用的話,可使用Find方法先找到實體,再修改,最後提交,這是沒問題的。可是在Web應用中就不行了,由於不能在兩個web服務器調用之間保留原始的上下文。咱們也不必尋找一個實體兩次,第一次用於展現給用戶,第二次用於更新。相反,咱們能夠直接修改實體的狀態達到目的。
由於咱們的例子不是web應用,因此這裏直接給出代碼了:
var province = new Province { Id = 1, ProvinceName = "山東省更新" }; province.Donators.Add(new Donator { Name = "醉、千秋",//再改回來 Id = 3, Amount = 12.00m, DonateDate = DateTime.Parse("2016/4/13 0:00:00"), }); using (var db = new DonatorsContext()) { db.Entry(province).State = EntityState.Modified; db.SaveChanges(); }
若是你也按照我這樣作了,你會發現省份表更新了,可是Donators表根本沒有修改爲功,這是由於EF內部的插入和更新底層實現是不一樣的。當把狀態設置爲Modified時,EF不會將這個改變傳播到整個對象圖。所以,要使代碼正常運行,須要再添加一點代碼:
using (var db = new DonatorsContext()) { db.Entry(province).State = EntityState.Modified; foreach (var donator in province.Donators) { db.Entry(donator).State = EntityState.Modified; } db.SaveChanges(); }
執行結果以下:
咱們須要手動處理的是要爲每一個發生變化的實體設置狀態。固然,若是要添加一個新的Donator,須要設置狀態爲Added
而不是Modified
。此外,還有更重要的一點,不管什麼時候使用這種狀態發生改變的方法時,咱們都必須知道全部列的數據(例如上面的例子),包括每一個實體的主鍵。這是由於當實體的狀態發生變化時,EF會認爲全部的屬性都須要更新。
一旦實體被附加到上下文,EF就會追蹤實體的狀態,這麼作是值得的。所以,若是你查詢了數據,那麼上下文就開始追蹤你的實體。若是你在寫一個web應用,那麼該追蹤就變成了一個查詢操做的沒必要要開銷,緣由是隻要web請求完成了獲取數據,那麼就會dispose上下文,並銷燬追蹤。EF有一種方法來減小這個開銷:
using (var context = new DonatorsContext()) { var provinceNormal = context.Provinces.Include(p => p.Donators); foreach (var p in provinceNormal) { Console.WriteLine("省份的追蹤狀態:{0}", context.Entry(p).State); foreach (var donator in p.Donators) { Console.WriteLine("打賞者的追蹤狀態:{0}", context.Entry(donator).State); } Console.WriteLine("**************"); } //使用AsNoTracking()方法設置再也不追蹤該實體 var province = context.Provinces.Include(p => p.Donators).AsNoTracking(); Console.WriteLine("使用了AsNoTracking()方法以後"); foreach (var p in province) { Console.WriteLine("省份的追蹤狀態:{0}", context.Entry(p).State); foreach (var donator in p.Donators) { Console.WriteLine("打賞者的追蹤狀態:{0}", context.Entry(donator).State); } Console.WriteLine("**************"); } }
從如下執行結果能夠看出,使用了AsNoTracking()
方法以後,實體的狀態都變成了Detached
,而沒有使用該方法時,狀態是Unchanged
。從以前的表中,咱們能夠知道,Unchanged至少數據庫上下文還在追蹤,只是追蹤到如今還沒發現它有變化,而Detached根本就沒有追蹤,這樣就減小了開銷。
若是在web應用中想更新用戶修改的屬性怎麼作?假設你在web客戶端必須跟蹤發生的變化而且拿到了變化的東西,那麼還可使用另外一種方法來完成更新操做,那就是使用DbSet的Attach
方法。該方法本質上是將實體的狀態設置爲Unchanged
,並開始跟蹤該實體。附加一個實體後,一次只能設置一個更改的屬性,你必須提早就知道哪一個屬性已經改變了。
var donator = new Donator { Id = 4, Name = "雪茄", Amount = 18.80m, DonateDate = DateTime.Parse("2016/4/15 0:00:00") }; using (var db = new DonatorsContext()) { db.Donators.Attach(donator); //db.Entry(donator).State=EntityState.Modified;//這句能夠做爲第二種方法替換上面一句代碼 donator.Name = "秦皇島-雪茄"; db.SaveChanges(); }
七、刪除數據
刪除和更新有不少類似之處,咱們可使用一個查詢找到數據,而後經過DbSet的Remove
方法將它標記爲刪除,這種方法也有和更新相同的缺點,會致使一個select查詢和一個刪除查詢。
static void Main(string[] args) { using (var db = new DonatorsContext()) { PrintAllDonators(db); Console.WriteLine("刪除後的數據以下:"); var toDelete = db.Provinces.Find(2); //刪除Donator toDelete.Donators.ToList().ForEach(d => db.Donators.Remove(d)); //刪除Province db.Provinces.Remove(toDelete); db.SaveChanges(); PrintAllDonators(db); } Console.WriteLine("finished"); Console.Read(); } //輸出全部的打賞者 private static void PrintAllDonators(DonatorsContext db) { var provinces = db.Provinces.ToList(); foreach (var province in provinces) { Console.WriteLine("{0}的打賞者以下:", province.ProvinceName); foreach (var donator in province.Donators) { Console.WriteLine("{0,-10}\t{1,-10}\t{2,-10}\t{3,-10}", donator.Id, donator.Name, donator.Amount, donator.DonateDate.ToShortDateString()); } } }
執行結果以下:
上面的代碼會刪除每一個子實體,而後再刪除根實體。刪除一個實體時必需要知道它的主鍵值,上面的代碼刪除了省份Id=2的數據。另外,可使用RemoveRange
方法刪除多個實體。
插入操做和刪除操做有一個很大的不一樣:刪除操做必需要手動刪除每一個子記錄,而插入操做不須要手動插入每一個子記錄,只須要插入父記錄便可。你也可使用級聯刪除操做來代替,可是許多DBA都不屑於級聯刪除。
下面,咱們經過爲每一個實體設置狀態來刪除實體,咱們仍是須要考慮每一個獨立的實體:
//方法2:經過設置實體狀態刪除 //id=1的省份是山東省,對應三個打賞者 var toDeleteProvince = new Province { Id = 1 }; toDeleteProvince.Donators.Add(new Donator { Id = 1 }); toDeleteProvince.Donators.Add(new Donator { Id = 2 }); toDeleteProvince.Donators.Add(new Donator { Id = 3 }); using (var db = new DonatorsContext()) { //刪除前先輸出現有的數據,不能寫在下面的using語句中,不然Attach方法會報錯,緣由我相信你已經能夠思考出來了 PrintAllDonators(db); } using (var db = new DonatorsContext()) { db.Provinces.Attach(toDeleteProvince); foreach (var donator in toDeleteProvince.Donators.ToList()) { db.Entry(donator).State = EntityState.Deleted; } //刪除完子實體再刪除父實體 db.Entry(toDeleteProvince).State = EntityState.Deleted; db.SaveChanges(); Console.WriteLine("刪除以後的數據以下:\r\n"); //刪除後輸出現有的數據 PrintAllDonators(db); }
執行效果以下:
毫無疑問你會發現刪除操做很是不一樣於其餘操做,要刪除一個省份,咱們只須要傳入它的主鍵便可,要刪除這個省份下的全部打賞者,咱們只須要在省份對象後追加要刪除的打賞者對象,並給每一個打賞者對象的Id屬性賦值便可。在web應用中,咱們須要提交全部的主鍵,或者須要查詢子記錄來找到對應的主鍵。
八、使用內存in-memory數據
有時,你須要在已存在的上下文中找到一個實體而不是每次都去數據庫去找。當建立新的上下文時,EF默認老是對數據庫進行查詢。
應用情景:若是你的更新調用了不少方法,而且你想知道以前的某個方法添加了什麼數據?這時,你可使用DbSet的Local
屬性強制執行一個只針對內存數據的查詢。
var query= db.Provinces.Local.Where(p => p.ProvinceName.Contains("東")).ToList();
Find
方法在構建數據庫查詢以前,會先去本地的上下文中搜索。這個很好證實,只須要找到加載不少條實體數據,而後使用Find
方法找到其中的一條便可。好比:
var provinces = db.Provinces.ToList(); //還剩Id=3和4的兩條數據了 var query = db.Provinces.Find(3);
打開Sql Server Profiler,能夠看到,只查詢了一次數據庫,並且仍是第一句代碼查詢的,這就證實了Find
方法首先去查詢內存中的數據。
經過ChangeTracker
對象,咱們能夠訪問內存中全部實體的狀態,也能夠查看這些實體以及它們的DbChangeTracker
。例如:
using (var db = new DonatorsContext()) { var provinces = db.Provinces.ToList(); //還剩Id=3和4的兩條數據了 var query = db.Provinces.Find(3); foreach (var entry in db.ChangeTracker.Entries<Province>()) { Console.WriteLine(entry.State); Console.WriteLine(entry.Entity.ProvinceName); } }
運行結果,能夠看到追蹤到的狀態等:
本章小結
首先,咱們看到了如何控制多個數據庫鏈接參數,如數據庫位置,數據庫名稱,模式等等,咱們也看到了如何使用數據庫初始化器建立數據庫初始化策略以知足應用程序的需求,最後,咱們看到了如何在EF Code First中使用數據庫初始化器來插入種子數據。
接下來,咱們看到了如何在EF中使用LINQ to Entities來查詢數據。咱們看到了使用EF的LINQ to Entities無縫地執行各類數據檢索任務。最後,咱們深刻介紹了EF Code First中的插入、更新和刪除!