本篇目錄
本篇的源碼下載:點擊下載
先附上codeplex上EF的源碼:entityframework.codeplex.com,此外,本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
上一篇《Code First開發系列之領域建模和管理實體關系》,我們主要介紹了EF中“約定大於配置”的概念,如何創建數據庫的表結構,以及如何管理實體間的三種關系和三種繼承模式。這一篇我們主要說三方面的問題,數據庫創建的管理,種子數據的填充以及CRUD的操作詳細用法。
管理數據庫創建
管理數據庫連接
使用配置文件管理連接
在數據庫上下文類中,如果我們只繼承了無參數的DbContext,並且在配置文件中創建了和數據庫上下文類同名的連接字符串,那么EF會使用該連接字符串自動計算出該數據庫的位置和數據庫名。比如,我們的上下文定義如下:
public class SampleDbEntities : DbContext
{
// Code here
}
如果我們在配置文件中定義的連接字符串如下:
<connectionStrings>
<add name="SampleDbEntities" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\myTestDb.mdf" providerName="System.Data.SqlClient" />
</connectionStrings>
這樣,EF會使用該連接字符串執行數據庫操作。究竟發生了什么呢?
當運行應用程序時,EF會尋找我們的上下文類名,即“SampleDbEntities”,並在配置文件中尋找和它同名的連接字符串,然后它會使用該連接字符串計算出應該使用哪個數據庫provider,之后檢查數據庫位置(例子中是當前的數據目錄),之后會在指定的位置創建一個名為myTestDb.mdf的數據庫文件,同時根據連接字符串的Initial Catalog屬性創建了一個名為myTestDb的數據庫。
使用配置文件指定數據庫位置和名字對於控制上下文類的連接參數也許是最簡單和最有效的方式,另一個好處是如果我們想為開發,生產和臨時環境創建各自的連接字符串,那么在配置文件中更改連接字符串並在開發時將它指向確定的數據庫也是一種方法。
這里要注意的重要的事情是在配置文件中定義的連接字符串具有最高優先權,它會覆蓋所有在其它地方指定的連接參數。
從最佳實踐的角度,也許不推薦使用配置文件。注入連接字符串是一種更好的方式,因為它給開發者更多的控制權和監管權。
使用已存在的ConnectionString
如果我們已經有了一個定義數據庫位置和名稱的ConnectionString,並且我們想在數據庫上下文類中使用這個連接字符串,如下:
<connectionStrings>
<add name="AppConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=testDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\testDb.mdf" providerName="System.Data.SqlClient" />
</connectionStrings>
那么我們可以將該連接字符串的名字傳入數據庫上下文DbContext的構造函數中,如下所示:
public class SampleDbEntities : DbContext
{
public SampleDbEntities()
:base("name=AppConnection")
{
}
}
上面的代碼將連接字符串的名字傳給了DbContext類的構造函數,這樣一來,我們的數據庫上下文就會開始使用連接字符串了。
如果在配置文件中還有一個和數據庫上下文類名同名的connectionString,那么就會使用這個同名的連接字符串。無論我們對傳入的連接字符串名稱如何改變,都是無濟於事的,也就是說和數據庫上下文類名同名的連接字符串優先權更大。
使用已存在的連接
通常在一些老項目中,我們只會在項目中的某個部分使用EF Code First,同時,我們想對數據上下文類使用已經存在的數據庫連接,如果要實現這個,可將連接對象傳給DbContext類的構造函數,如下:
public class SampleDbEntities : DbContext
{
public SampleDbEntities( DbConnection con ) : base( con, contextOwnsConnection : false )
{
}
}
這里要注意一下contextOwnsConnection
參數,之所以將它作為false傳入到上下文,是因為它是從外部傳入的,當上下文出了范圍時,可能會有人想要使用該連接。如果傳入true的話,那么一旦上下文出了范圍,數據庫連接就會立即關閉。
管理數據庫初始化
首次運行EF Code First應用時,EF會做下面的這些事情:
- 檢查正在使用的
DbContext
類。 - 找到該上下文類使用的
connectionString
。 - 找到領域實體並提取模式相關的信息。
- 創建數據庫。
- 將數據插入系統。
一旦模式信息提取出來,EF會使用數據庫初始化器將該模式信息推送給數據庫。數據庫初始化器有很多可能的策略,EF默認的策略是如果數據庫不存在,那么就重新創建;如果存在的話就使用當前存在的數據庫。當然,我們有時也可能需要覆蓋默認的策略,可能用到的數據庫初始化策略如下:
CreateDatabaseIfNotExists
:顧名思義,如果數據庫不存在,那么就重新創建,否則就使用現有的數據庫。如果從領域模型中提取到的模式信息和實際的數據庫模式不匹配,那么就會拋出異常。DropCreateDatabaseAlways
:如果使用了該策略,那么每次運行程序時,數據庫都會被銷毀。這在開發周期的早期階段通常很有用(比如設計領域實體時),從單元測試的角度也很有用。DropCreateDatabaseIfModelChanges
:這個策略的意思就是說,如果領域模型發生了變化(具體而言,從領域實體提取出來的模式信息和實際的數據庫模式信息失配時),就會銷毀以前的數據庫(如果存在的話),並創建新的數據庫。MigrateDatabaseToLatestVersion
:如果使用了該初始化器,那么無論什么時候更新實體模型,EF都會自動地更新數據庫模式。這里很重要的一點是,這種策略更新數據庫模式不會丟失數據,或者是在已有的數據庫中更新已存在的數據庫對象。
MigrateDatabaseToLatestVersion
初始化器只有從EF 4.3才可用。
設置初始化策略
EF默認使用CreateDatabaseIfNotExists
作為默認初始化器,如果要覆蓋這個策略,那么需要在DbContext類中的構造函數中使用Database.SetInitializer
方法,下面的例子使用DropCreateDatabaseIfModelChanges
策略覆蓋默認的策略:
public class SampleDbEntities : DbContext
{
public SampleDbEntities()
: base( "name=AppConnection" )
{
Database.SetInitializer<SampleDbEntities>( new DropCreateDatabaseIfModelChanges<SampleDbEntities>() );
}
}
這樣一來,無論什么時候創建上下文類,Database.SetInitializer
方法都會被調用,並且將數據庫初始化策略設置為DropCreateDatabaseIfModelChanges
。
如果處於生產環境,那么我們肯定不想丟失已存在的數據。這時我們就需要關閉該初始化器,只需要將null
傳給Database.SetInitializer
方法,如下所示:
public class SampleDbEntities : DbContext
{
public SampleDbEntities()
: base( "name=AppConnection" )
{
Database.SetInitializer<SampleDbEntities>(null);
}
}
填充種子數據
到目前為止,無論我們選擇哪種策略初始化數據庫,生成的數據庫都是一個空的數據庫。但是許多情況下我們總想在數據庫創建之后、首次使用之前就插入一些數據,此外,開發階段可能想以admin的資格為其填充一些數據,或者為了測試應用在特定的場景中表現如何,想要偽造一些數據。
當我們使用DropCreateDatabaseAlways
和DropCreateDatabaseIfModelChanges
初始化策略時,插入種子數據非常重要,因為每次運行應用時,數據庫都要重新創建,每次數據庫創建之后再手動插入數據非常乏味。接下來我們看一下當數據庫創建之后如何使用EF來插入種子數據。
為了向數據庫插入一些初始化數據,我們需要創建滿足下列條件的數據庫初始化器類:
- 從已存在的數據庫初始化器類中派生數據
- 在數據庫創建期間種子化
1.0 定義領域實體
假設我們的數據模型Employer定義如下:
public class Employer
{
public int Id { get; set; }
public string EmployerName { get; set; }
}
2.0 創建數據庫上下文
使用EF的Code First方法對上面的模型創建數據庫上下文:
public class SeedingDataContext:DbContext
{
public virtual DbSet<Employer> Employers { get; set; }
}
3.0 創建數據庫初始化器類
假設我們使用的是DropCreateDatabaseAlways
數據庫初始化策略,那么初始化器類就要從該泛型類繼承,並傳入數據庫上下文作為類型參數。接下來,要種子化數據庫就要重寫DropCreateDatabaseAlways
類的Seed
方法,而Seed方法拿到了數據庫上下文,因此我們可以使用它來將數據插入數據庫:
public class SeedingDataInitializer:DropCreateDatabaseAlways<SeedingDataContext>
{
protected override void Seed(SeedingDataContext 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()
,因為它會在基類中自動調用。
4.0 將數據庫初始化器類用於數據庫上下文類
public class SeedingDataContext:DbContext
{
public virtual DbSet<Employer> Employers { get; set; }
protected SeedingDataContext()
{
Database.SetInitializer<SeedingDataContext>(new SeedingDataInitializer());
}
}
5.0 Main方法中訪問數據庫
class Program
{
static void Main(string[] args)
{
using (var db = new SeedingDataContext())
{
var employers = db.Employers;
foreach (var employer in employers)
{
Console.WriteLine("Id={0}\tName={1}",employer.Id,employer.EmployerName);
}
}
Console.WriteLine("DB創建成功,並完成種子化!");
Console.Read();
}
}
6.0 運行程序,查看效果
Main方法中只是簡單的創建了數據庫上下文對象,然后將數據讀取出來:
此外,我們可以從數據庫初始化的Seed
方法中,通過數據庫上下文類給數據庫傳入原生SQL來影響數據庫模式。
樓主在實踐的過程中,遇到了以下問題,以后可能還會遇到,你也很可能遇到:程序運行時沒有錯誤(有時不會生成數據庫),但在調試時就報下面的錯誤:
The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.
提示的錯誤信息很清楚,但是依然沒有找到問題的根本所在,網上也沒找到滿意的答案(有說更改連接字符串的,有說更新到最新版的EF等等),如果有解決了這個問題園友,歡迎留言!
LINQ to Entities詳解
到目前為止,我們已經學會了如何使用Code First方式來創建實體數據模型,也學會了使用EF進行領域建模,執行模型驗證以及控制數據庫連接參數。一旦數據建模完成,接下來就是要對這些模型進行各種操作了,通常有以下兩種方式:
- LINQ to Entities
- Entity SQL
本系列教程只講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如何執行該查詢並給應用程序返回結果:
- 應用程序創建一個LINQ查詢。
- LINQ to Entities會將該LINQ查詢轉換成
EntityClient
命令。 EntityClient
命令然后使用EF和實體數據模型將這些命令轉換成SQL查詢。- 然后會使用底層的ADO.NET provider將該SQL查詢傳入數據庫。
- 該查詢然后在數據庫中執行。
- 執行結果返回給EF。
- EF然后將返回的結果轉成CLR類型,比如領域實體。
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=DonatorsConn")
{
}
public virtual DbSet<Donator> Donators { get; set; }
}
定義好連接字符串之后,如果使用該實體數據模型通過執行LINQ查詢來獲取Donator數據,那么可以在數據庫上下文類的Donators集合上操作。下面我們用兩種方法來實現“找出打賞了50元的打賞者”。
查詢語法
var donators = from donator in db.Donators
where donator.Amount == 50m
select donator;
方法語法
var donators = db.Donators.Where(d => d.Amount == 50m);
完整的Main方法如下:
class Program
{
static void Main(string[] args)
{
using (var db=new DonatorsContext())
{
//1.查詢語法
//var donators = from donator in db.Donators
// where donator.Amount == 50m
// 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 HashSet<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 db.Donators
select donator;
//2.方法語法
var donators = db.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。
使用導航屬性
如果實體間存在一種關系,那么這個關系是通過它們各自實體的導航屬性進行暴露的。在上面的例子中,省份Province
實體有一個Donators集合屬性用於返回該省份的所有打賞者,而在打賞者Donator
實體中,也有一個Province屬性用於跟蹤該打賞者屬於哪個省份。導航屬性簡化了從一個實體到和它相關的實體,下面我們看一下如何使用導航屬性獲取與其相關的實體數據。
比如,我們想要獲取“山東省的所有打賞者”:
#region 2.0 使用導航屬性
//1 查詢語法
var donators = from province in db.Provinces
where province.ProvinceName == "山東省"
from donator in province.Donators
select donator;
//2 方法語法
var donators = db.Provinces.Where(p => p.ProvinceName == "山東省").SelectMany(p => p.Donators);
#endregion
最終的查詢結果都是一樣的:
反過來,如果我們想要獲取打賞者“雪茄”的省份:
//1 查詢語法
//var province = from donator in db.Donators
// where donator.Name == "雪茄"
// select donator.Province;
//2 方法語法
var province = db.Donators.Where(d => d.Name == "雪茄").Select(d => d.Province);
下面是“找出山東省的所有打賞者”的LINQ語句生成的SQL,可見,EF可以使用LINQ查詢中的導航屬性幫我們創建適當的SQL來獲取數據。
SELECT
[Extent1].[Id] AS [Id],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name],
[Extent2].[Amount] AS [Amount],
[Extent2].[DonateDate] AS [DonateDate],
[Extent2].[Province_Id] AS [Province_Id]
FROM [dbo].[Provinces] AS [Extent1]
INNER JOIN [dbo].[Donators] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Province_Id]
WHERE N'山東省' = [Extent1].[ProvinceName]
過濾數據
實際上之前已經介紹了,根據某些條件過濾數據,可以在LINQ查詢中使用Where
。比如上面我們查詢了山東省的所有打賞者,這里我們過濾出打賞金額在10~20元之間的打賞者:
#region 3.0 過濾數據
//1 查詢語法
var donators = from donator in db.Donators
where donator.Amount > 10 && donator.Amount < 20
select donator;
//2 方法語法
var donators = db.Donators.Where(d => d.Amount > 10 && d.Amount < 20);
#endregion
最終查詢的結果如下:
生成的SQL語句在這里不在貼出來了,大家自己通過LINQPad或者其他工具自己去看吧!只要知道EF會幫助我們自動將LINQ查詢轉換成合適的SQL語句就可以了。
LINQ投影
如果不指定投影的話,那么默認就是選擇該實體或與之相關實體的所有字段,LINQ投影就是返回這些實體屬性的子集或者返回一個包含了多個實體的某些屬性的對象。
投影一般用在應用程序中的VIewModel(視圖模型),我們可以從LINQ查詢中直接返回一個視圖模型。比如,我們想要查出“所有省的所有打賞者”:
#region 4.0 LINQ投影
//1 查詢語法
var donators = from province in db.Provinces
select new
{
Province=province.ProvinceName,
DonatorList=province.Donators
};
//2 方法語法
var donators =db.Provinces.Select(p=>new
{
Province = p.ProvinceName,
DonatorList = p.Donators
});
Console.WriteLine("省份\t打賞者");
foreach (var donator in donators)
{
foreach (var donator1 in donator.DonatorList)
{
Console.WriteLine("{0}\t{1}", donator.Province,donator1.Name);
}
}
#endregion
執行結果如下:
當然,如果我們已經定義了一個包含了Province
和DonatorList
屬性的類型(比如視圖模型),那么也可以直接返回該類型,下面只給出方法語法(查詢語法大家可自行寫出)的寫法:
public class DonatorsWithProvinceViewModel
{
public string Province { get; set; }
public ICollection<Donator> DonatorList { get; set; }
}
//3 返回一個對象的方法語法
var donators = db.Provinces.Select(p => new DonatorsWithProvinceViewModel
{
Province = p.ProvinceName,
DonatorList = p.Donators
});
在
IQueryable<T>
中處理結果也會提升性能,因為直到要查詢的結果進行枚舉時才會執行生成的SQL。
分組Group
分組的重要性相必大家都知道,這個肯定是要掌握的!下面就看看兩種方法的寫法。
#region 5.0 分組Group
//1 查詢語法
var donatorsWithProvince = from donator in db.Donators
group donator by donator.Province.ProvinceName
into donatorGroup
select new
{
ProvinceName=donatorGroup.Key,
Donators=donatorGroup
};
//2 方法語法
var donatorsWithProvince = db.Donators.GroupBy(d => d.Province.ProvinceName)
.Select(donatorGroup => new
{
ProvinceName = donatorGroup.Key,
Donators = donatorGroup
});
foreach (var dwp in donatorsWithProvince)
{
Console.WriteLine("{0}的打賞者如下:",dwp.ProvinceName);
foreach (var d in dwp.Donators)
{
Console.WriteLine("{0}\t\t{1}", d.Name, d.Amount);
}
}
#endregion
稍微解釋一下吧,上面的代碼會根據省份名稱進行分組,最終以匿名對象的投影返回。結果中的ProvinceName就是分組時用到的字段,Donators屬性包含了通過ProvinceName找到的Donator集合。
執行結果如下:
排序Ordering
對特定的列進行升序或降序排列也是經常使用的操作。比如我們按照打賞金額進行排序。
按照打賞金額升序排序(LINQ sql查詢中ascending
關鍵字可省略):
#region 6.0 排序Ordering
//1 升序查詢語法
var donators = from donator in db.Donators
orderby donator.Amount ascending //ascending可省略
select donator;
//2 升序方法語法
var donators = db.Donators.OrderBy(d => d.Amount);
//#endregion
//1 降序查詢語法
var donators = from donator in db.Donators
orderby donator.Amount descending
select donator;
//2 降序方法語法
var donators = db.Donators.OrderByDescending(d => d.Amount);
#endregion
升序查詢執行結果:
降序查詢執行結果:
聚合操作
使用LINQ to Entities可以執行下面的聚合操作:
- Count-數量
- Sum-求和
- Min-最小值
- Max-最大值
- Average-平均值
下面我找出山東省打賞者的數量:
#region 7.0 聚合操作
var count = (from donator in db.Donators
where donator.Province.ProvinceName == "山東省"
select donator).Count();
var count2 = db.Donators.Count(d => d.Province.ProvinceName == "山東省");
Console.WriteLine("查詢語法Count={0},方法語法Count={1}",count,count2);
#endregion
執行結果見下圖,可見,方法語法更加簡潔,而且查詢語法還要將前面的LINQ sql用括號括起來才能進行聚合(其實這是混合語法),沒有方法語法簡單靈活,所以下面的幾個方法我們只用方法語法進行演示。
其他聚合函數的代碼:
var sum = db.Donators.Sum(d => d.Amount);//計算所有打賞者的金額總和
var min = db.Donators.Min(d => d.Amount);//最少的打賞金額
var max = db.Donators.Max(d => d.Amount);//最多的打賞金額
var average = db.Donators.Average(d => d.Amount);//打賞金額的平均值
Console.WriteLine("Sum={0},Min={1},Max={2},Average={3}",sum,min,max,average);
執行結果:
分頁Paging
分頁也是提升性能的一種方式,而不是將所有符合條件的數據一次性全部加載出來。在LINQ to Entities中,實現分頁的兩個主要方法是:Skip
和Take
,這兩個方法在使用前都要先進行排序,切記。
Skip
該方法用於從查詢結果中跳過前N條數據。假如我們根據Id排序后,跳過前2條數據:
#region 8.0 分頁Paging
var donatorsBefore = db.Donators;
var donatorsAfter = db.Donators.OrderBy(d => d.Id).Skip(2);
Console.WriteLine("原始數據打印結果:");
PrintDonators(donatorsBefore);
Console.WriteLine("Skip(2)之后的結果:");
PrintDonators(donatorsAfter);
#endregion
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條打賞者:
var take = db.Donators.Take(3);
Console.WriteLine("Take(3)之后的結果:");
PrintDonators(take);
執行結果:
分頁實現
如果我們要實現分頁功能,那么我們必須在相同的查詢中同時使用Skip和Take方法。
由於現在我數據庫只有5條打賞者的數據,所以我打算每頁2條數據,這樣就會有3頁數據。
//分頁實現
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 = db.Donators.OrderBy(d => d.Id).Skip((page - 1)*pageSize).Take(pageSize);
PrintDonators(donators);
}
else
{
break;
}
}
和聚合函數一樣,分頁操作只有方法語法。
實現多表連接join
如果兩個實體之間是互相關聯的,那么EF會在實體中創建一個導航屬性來訪問相關的實體。也可能存在一種情況,兩個實體之間有公用的屬性,但是沒有在數據庫中定義它們間的關系。如果我們要使用該隱式的關系,那么可以連接相關的實體。
但是之前我們創建實體類時已經給兩個實體建立了一對多關系,所以這里我們使用導航屬性模擬join連接:
#region 9.0實現多表連接
var join1 = from province in db.Provinces
join donator in db.Donators on province.Id equals donator.Province.Id
into donatorList//注意,這里的donatorList是屬於某個省份的所有打賞者,很多人會誤解為這是兩張表join之后的結果集
select new
{
ProvinceName = province.ProvinceName,
DonatorList = donatorList
};
var join2 = db.Provinces.GroupJoin(db.Donators,//Provinces集合要連接的Donators實體集合
province => province.Id,//左表要連接的鍵
donator => donator.Province.Id,//右表要連接的鍵
(province, donatorGroup) => new//返回的結果集
{
ProvinceName = province.ProvinceName,
DonatorList = donatorGroup
}
);
#endregion
LINQ中的
join
和GroupJoin
相當於SQL中的Left Outer Join
。無論右邊實體集合中是否包含任何實體,它總是會返回左邊集合的所有元素。
懶加載和預加載
使用LINQ to Entities時,理解懶加載和預加載的概念很重要。因為理解了這些,就會很好地幫助你編寫有效的LINQ查詢。
懶加載
懶加載是這樣一種過程,直到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=db.Donators;
要真正從數據庫中加載數據,我們要枚舉donators
,通過ToList()方法或者在foreach循環中遍歷都可以。
看下面的代碼解釋:
#region 10.0 懶加載
var donators = db.Donators;//還沒有查詢數據庫
var donatorList = donators.ToList();//已經查詢了數據庫,但由於懶加載的存在,還沒有加載Provinces表的數據
var province = donatorList.ElementAt(0).Province;//因為用戶訪問了Province表的數據,因此這時才加載
#endregion
使用Code First時,懶加載依賴於導航屬性的本質。如果導航屬性是virtual
修飾的,那么懶加載就開啟了,如果要關閉懶加載,不要給導航屬性加virtual
關鍵字就可以了。
如果想要為所有的實體關閉懶加載,那么可以在數據庫中的上下文中去掉實體集合屬性的virtual
關鍵字即可。
預加載
預加載是這樣一種過程,當我們要加載查詢中的主要實體時,同時也加載與之相關的實體。要實現預加載,我們要使用Include
方法。下面我們看一下如何在加載Donator數據的時候,同時也預先加載所有的Provinces數據:
//預加載,以下兩種方式都可以
var donators2 = db.Donators.Include(d => d.Province).ToList();
var donators3 = db.Donators.Include("Provinces").ToList();
這樣,當我們從數據庫中取到Donators集合時,也取到了Provinces集合。
插入數據
將新的數據插入數據庫有多種方法,可以使用之前的Add
方法,也可以給每個實體的狀態設置為Added
。如果你要添加的實體包含子實體,那么Added
狀態會擴散到該圖的所有對象中。換言之,如果根實體是新的,那么EF會假定你附加了一個新的對象圖。該對象圖一般指的是許多相關的實體形成的一個復雜的樹結構。比如,比如我們有一個Province對象,每個省份有很多打賞者Donators,包含在Province類的List屬性中,那么我們就是在處理一個對象圖,本質上,Donator
實體是person對象的孩子。
首先,我們創建一個新的具有打賞者的Province實例,然后,我們把該實例添加到數據庫上下文中,最后,調用SaveChanges
將數據行提交到數據庫:
#region 11.0 插入數據
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();
}
#endregion
這和之前看到的代碼還是有些不同的。我們在初始化上下文之前就創建了對象,這個表明了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的列作為主鍵,因此我們只需要傳一個參數。如果你使用了復合主鍵(包含了不止一列,常見於連接表),就需要傳入每列的值,並且主鍵列的順序要准確。
#region 12.0 更新數據
using (var db=new DonatorsContext())
{
var donator = db.Donators.Find(3);
donator.Name = "醉千秋";//我想把“醉、千秋”中的頓號去掉
db.SaveChanges();
}
#endregion
如果執行了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 db=new DonatorsContext())
{
var provinceNormal = db.Provinces.Include(p => p.Donators);
foreach (var p in provinceNormal)
{
Console.WriteLine("省份的追蹤狀態:{0}", db.Entry(p).State);
foreach (var donator in p.Donators)
{
Console.WriteLine("打賞者的追蹤狀態:{0}", db.Entry(donator).State);
}
Console.WriteLine("**************");
}
var province = db.Provinces.Include(p => p.Donators).AsNoTracking();//使用AsNoTracking()方法設置不再追蹤該實體
Console.WriteLine("使用了AsNoTracking()方法之后");
foreach (var p in province)
{
Console.WriteLine("省份的追蹤狀態:{0}", db.Entry(p).State);
foreach (var donator in p.Donators)
{
Console.WriteLine("打賞者的追蹤狀態:{0}",db.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查詢和一個刪除查詢。
#region 13.0 刪除數據
using (var db = new DonatorsContext())
{
PrintAllDonators(db);
Console.WriteLine("刪除后的數據如下:");
var toDelete = db.Provinces.Find(2);
toDelete.Donators.ToList().ForEach(
d => db.Donators.Remove(d));
db.Provinces.Remove(toDelete);
db.SaveChanges();
PrintAllDonators(db);
}
#endregion
//輸出所有的打賞者
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:通過設置實體狀態刪除
var toDeleteProvince = new Province { Id = 1 };//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())
{
PrintAllDonators(db);//刪除前先輸出現有的數據,不能寫在下面的using語句中,否則Attach方法會報錯,原因我相信你已經可以思考出來了
}
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
方法找到其中的一條即可。比如:
#region 14.0 使用內存數據
using (var db=new DonatorsContext())
{
var provinces = db.Provinces.ToList();
var query = db.Provinces.Find(3);//還剩Id=3和4的兩條數據了
}
#endregion
打開Sql Server Profiler,可以看到,只查詢了一次數據庫,而且還是第一句代碼查詢的,這就證明了Find
方法首先去查詢內存中的數據。
通過ChangeTracker
對象,我們可以訪問內存中所有實體的狀態,也可以查看這些實體以及它們的DbChangeTracker
。例如:
using (var db=new DonatorsContext())
{
//14.1 證明Find方法先去內存中尋找數據
var provinces = db.Provinces.ToList();
//var query = db.Provinces.Find(3);//還剩Id=3和4的兩條數據了
//14.2 ChangeTracker的使用
foreach (var dbEntityEntry in db.ChangeTracker.Entries<Province>())
{
Console.WriteLine(dbEntityEntry.State);
Console.WriteLine(dbEntityEntry.Entity.ProvinceName);
}
}
#endregion
運行結果,可以看到追蹤到的狀態等:
本章小結
首先,我們看到了如何控制多個數據庫連接參數,如數據庫位置,數據庫名稱,模式等等,我們也看到了如何使用數據庫初始化器創建數據庫初始化策略以滿足應用程序的需求,最后,我們看到了如何在EF Code First中使用數據庫初始化器來插入種子數據。
接下來,我們看到了如何在EF中使用LINQ to Entities來查詢數據。我們看到了使用EF的LINQ to Entities無縫地執行各種數據檢索任務。最后,我們深入介紹了EF Code First中的插入、更新和刪除!
自我測試
- LINQ不支持下列拿哪種語法?
- 方法
- SQL
- 查詢
- 通過LINQ從數據庫中檢索到一個實體,然后更改部分屬性的值,再調用
SaveChanges
,所有的屬性都會更新到數據庫而不僅僅是修改后的屬性,對嗎? - 要給多個屬性排序,你只需要在LINQ的方法語法中多次調用
OrderBy
就行了嗎? - 如何在LINQ的查詢語法中添加兩個過濾條件?
- 使用多個where調用
- 使用and邏輯操作符
- 執行兩個查詢
- 如何給
DbSet
添加多個新的實體?- 調用
Add
方法,然后傳入數據庫上下文屬性識別的類的實例 - 調用
AddRange
方法,並傳入目標實體類型的枚舉 - 在每個新實體上使用數據庫上下文API,將狀態設置為
Added
- 以上所有
- 調用
- 如果想創建一個具有多個子實體的新實體,也叫做對象圖,那么為了持久化該對象圖,必須要在父親和每個孩子實體上調用
Add
嗎? - 如果將一個實體的狀態設置為
Modified
,那么當調用SaveChanges
之后,相應表的所有列都會更新嗎? - 為了觸發一個刪除查詢執行,你需要在
SaveChanges
時調用Add
,然后調用Remove
,對嗎? - 調用
SaveChanges
調用時,哪個實體狀態不會導致數據庫查詢?- Added
- Detached
- Deleted
- Modified
DbSet
的哪個屬性讓你可以訪問已經從數據庫加載到上下文中的實體?- Memory
- Local
- Loaded
如果您覺得這篇文章對您有價值或者有所收獲,請點擊右下方的店長推薦,然后查看答案,謝謝!
參考書籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》