本篇目錄
上一篇《8天掌握EF的Code First開發之Entity Framework介紹》,只是大概地從整體上了解了一下Entity Framework,純粹理論,沒有一點代碼,但是推薦數量飆升。博主因此也感覺到了某些園友們的氣息里透漏着些許火葯味,確實沒有啥干貨啊,這個博主承認的,博主也請各位諒解,並聽我給你解釋解釋。
- 這是博主對於EF技術的試水篇。博主畢竟要寫EF這個系列的文章,先來探探園友們對EF的感冒程度啊。也算是一種需求分析或者市場調研吧!
- 這是博主對“大家是否喜歡自測功能”的試水。上一篇博客的最后添加了自我測試,是想看看有多少人喜歡這個功能,同時也是希望在自我測試時能不受正確答案的干擾。
- 從這篇博客起,我會在您點擊查看答案前提供明確的提示!我也在此聲明,不想點贊的園友千萬不要點擊查看答案按鈕,否則您會失誤點了推薦的!:)
下面我們正式開始今天的正題。本篇的源碼:點擊查看,本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
創建控制台項目
新建控制台應用項目

安裝Entity Framework
Nuget包管理器控制台 :Install-Package EntityFramework,也可以通過可視化窗口進行安裝,如下

根據.Net中的類來創建數據庫
上面的步驟之后,我們就可以開始寫代碼了。在寫代碼之前,你要始終記得,每個類就是相應的數據表中的一行數據,該類的屬性對應的是這行數據的列。為了表示贊助者們對我的支持,我決定用他們的數據進行舉例。

創建EDM實體數據模型
我們現在創建一個捐贈者類Donator:
namespace FirstCodeFirstApp
{
public class Donator
{
public int DonatorId { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
}
}
我們需要定義和期望的數據庫類型相匹配的屬性。上面的例子中,.Net中的int類型會映射到SQL Server中的int類型,string類型會映射到所有可能的字符類型,decimal和Datetime也和SQL Server中的一樣。大多數時候,我們不需要關心這些細節,我們只需要編寫能夠表示數據的模型類就行了,然后使用標准的.Net類型定義屬性,其他的就讓EF自己計算出保存數據所需要的RDBMS類型。
創建數據庫上下文
接下來我們創建數據庫上下文,它是數據庫的抽象。目前,我們只有一張表Donator,因而要給該數據庫上下文定義一個屬性來代表這張表。再者,一張表中一般肯定不止一條數據行,所以我們必須定義一個集合屬性,EF使用DbSet來實現這個目的。
namespace FirstCodeFirstApp
{
public class Context:DbContext
{
public Context()
: base("name=FirstCodeFirstApp")
{
}
public DbSet<Donator> Donators { get; set; }
}
}
在這里,DbContext是所有基於EF的上下文基類,通過它可以訪問到數據庫中的所有表。上面的代碼中調用了父類的構造函數,並且傳入了一個鍵值對,鍵是name,值是FirstCodeFirstApp,這個鍵值對是定義在應用程序的配置文件中的,取決於你的應用程序類型,可能是app.config或者web.config。在我們的控制台應用程序中就是app.config。
在app.config文件的configuration的節點下(不要在第一個節點下,否則報錯)添加:
<connectionStrings>
<add name="FirstCodeFirstApp" connectionString="Server=.;Database=CodeFirstApp;Integrated Security=SSPI" providerName="System.Data.SqlClient"/>
</connectionStrings>
接下來就應該是創建數據庫了,創建數據庫的方式有兩種:
- 在后面的博客中我們會通過數據庫遷移來實現數據庫的創建,原理就是數據庫會在第一次查詢,更新或插入操作時創建。
- 通過EF數據庫的API來創建。
這里我們先通過第二種方法來創建數據庫。首先我們必須確保數據庫中沒有和我們現在要創建的數據庫同名的數據庫存在,否則會提示錯誤。當然,我們也可以通過EF的API訪問數據庫來創建數據庫。
namespace FirstCodeFirstApp
{
class Program
{
static void Main(string[] args)
{
using (var context=new Context())
{
context.Database.CreateIfNotExists();//如果數據庫不存在時則創建
}
Console.Write("DB has Created!");//提示DB創建成功
Console.Read();
}
}
}
最后,我們只需要確保我們的連接字符串沒有問題。現在,運行程序,打開SSMS(當然也可以在VS的服務器資源管理器查看)進行確認即可。


可以很清楚地看到,數據表名是自定義數據庫上下文的屬性,而表中的列是數據模型的屬性。此外,注意一下列的類型。EF默認將DonatorId作為了主鍵,string類型的Name在數據庫中的類型是nvarchar(max),這些都是在使用EF時必須注意的命名規范(或者約定)。
簡單的CRUD操作
首先,大腦中時刻要有這張圖,這張圖也是很重要的概念:

創建記錄——Create
你可以這樣認為,將對象添加到集合中就相當於將數據行插入到數據庫的相應的表中。我們使用DbSet的Add方法來實現新數據的添加,而DbContext類的SaveChanges方法會將未處理的更改提交到數據庫,這是通過檢測上下文中所有的對象的狀態來完成的。所有的對象都駐留在上下文類的DbSet屬性中,比如,我們的例子只有一個Donators屬性,那么所有的捐贈人的數據都會存儲到這個泛型集合屬性中。數據庫上下文會跟蹤DbSet屬性中的所有對象的狀態,這些狀態有這么幾種:Deleted,Added,Modified和Unchanged。如果你想在一個表中插入多行數據,那么只需要添加該表對應的類的多個對象的實例即可,然后就使用SaveChanges方法將更改提交到數據庫,該方法是以單事務執行的。最終,所有的數據庫更改都會以單個工作單元持久化。既然是事務的,那么這就允許將批量相關的更改作為單個操作提交,這樣就保證了事務一致性和數據完整性。
修改Main方法如下:
class Program
{
static void Main(string[] args)
{
using (var context = new Context())
{
context.Database.CreateIfNotExists();//如果數據庫不存在時則創建
var donators = new List<Donator>
{
new Donator
{
Name = "陳志康",
Amount = 50,
DonateDate = new DateTime(2016, 4, 7)
},
new Donator
{
Name = "海風",
Amount = 5,
DonateDate = new DateTime(2016, 4, 8)
},
new Donator
{
Name = "醉千秋",
Amount = 18.8m,
DonateDate = new DateTime(2016, 4, 15)
}
};
context.Donators.AddRange(donators);
context.SaveChanges();
}
// Console.Write("DB has Created!");//提示DB創建成功
Console.Write("Creation Finished!");//提示創建完成
Console.Read();
}
}
這里需要注意兩點:
- 不需要給DonatorId屬性賦值,因為它對應到SQL Server表中的主鍵列,它的值是自動生成的,當
SaveChanges執行之后,打個斷點就能看到返回的DonatorId已經有值了。 - Context的實例用了using語句包裝起來,這是因為DbContext實現了IDisposable接口。Dbcontext還包含了DbConnection的實例,該實例指向了具有特定連接字符串的數據庫。在EF中合適地釋放數據庫連接和ADO.NET中同等重要。
執行結果與在VS中查看數據是否生成:

查詢記錄——Retrieve
查詢時也是直接通過DbSet進行查詢的。
#region 2.0 查詢記錄
var donators = context.Donators;
Console.WriteLine("Id\t\t姓名\t\t金額\t\t贊助日期");
foreach (var donator in donators)
{
Console.WriteLine("{0}\t\t{1}\t\t{2}\t\t{3}", donator.DonatorId,donator.Name, donator.Amount, donator.DonateDate.ToShortDateString());
}
#endregion
如果像下面那樣打一個斷點,你會看到一個結果視圖,點擊那個類似刷新的圖標會看到查詢的結果,這個東西道出了EF中很重要的一個概念: 延遲查詢。但是此時還沒有真正查詢數據庫,只有當LINQ的查詢結果被訪問或者枚舉時才會將查詢命令發送到數據庫。EF是基於Dbset實現的IQueryable接口來處理延遲查詢的。

最后,我使用了一個foreach循環將結果枚舉出來,這樣就執行了SQL,結果如下:

物質化:Materialization
從數據庫中讀取數據,通過
DbDataReader轉成.Net對象的過程。
更新記錄——Update
在SQL中,更新需要使用Update命令。而在EF中,我們要找到DbSet集合中要更新的對象,然后更改其屬性,最后調用SaveChanges方法即可。
下面我將贊助者“醉千秋”改為“醉、千秋”(因為他的博客園名字的確是這樣的)。
#region 3.0 更新記錄
var donators = context.Donators;
if (donators.Any())
{
var toBeUpdatedDonator = donators.First(d => d.Name == "醉千秋");
toBeUpdatedDonator.Name = "醉、千秋";
context.SaveChanges();
}
#endregion
這里我們使用了Any()擴展方法來判斷序列中是否有元素,然后使用First()擴展方法來找到Name == "醉千秋"的元素,最后給目標對象的Name屬性賦予新值,之后調用SaveChanges方法。
如果我們打開SQL Server Profiler,我們會看到下面的記錄:


第一條記錄是查詢語句,找到對象;第二條是更新語句,更改屬性。最后檢測數據庫操作成功的結果:

刪除記錄——Delete
接下來要刪除記錄,我把剩下的打賞者數據全部放到數據庫,然后再在最后加入一條測試數據,如下:
INSERT dbo.Donators VALUES ( N'雪茄', 10, '2016-04-08')
INSERT dbo.Donators VALUES ( N'王小乙', 10, '2016-04-09')
INSERT dbo.Donators VALUES ( N'鍵盤里的鼠標', 12, '2016-04-13')
INSERT dbo.Donators VALUES ( N'smallpig', 10, '2016-04-13')
INSERT dbo.Donators VALUES ( N'Darren', 5, '2016-04-15')
INSERT dbo.Donators VALUES ( N'jeffrey', 10, '2016-04-15')
INSERT dbo.Donators VALUES ( N'危楊益', 6.66, '2016-04-15')
INSERT dbo.Donators VALUES ( N'Mr.Lan', 10, '2016-04-15')
INSERT dbo.Donators VALUES ( N'周旭龍', 5, '2016-04-15')
INSERT dbo.Donators VALUES ( N'403', 10.24, '2016-04-15')
INSERT dbo.Donators VALUES ( N'cuibty', 8.88, '2016-04-15')
INSERT dbo.Donators VALUES ( N'dennylo', 10.24, '2016-04-17')
INSERT dbo.Donators VALUES ( N'lee', 5, '2016-04-18')
INSERT dbo.Donators VALUES ( N'利平', 18.8, '2016-04-18')
INSERT dbo.Donators VALUES ( N'聽海船說', 20, '2016-04-19')
INSERT dbo.Donators VALUES ( N'喝前搖一搖', 5, '2016-04-19')
INSERT dbo.Donators VALUES ( N'黃大仙', 50, '2016-04-19')
INSERT dbo.Donators VALUES ( N'夜未眠', 10, '2016-04-19')
INSERT dbo.Donators VALUES ( N'A.L', 8.88, '2016-04-19')
INSERT dbo.Donators VALUES ( N'transient', 5, '2016-04-19')
INSERT dbo.Donators VALUES ( N'曉東', 6.66, '2016-04-20')
INSERT dbo.Donators VALUES ( N'待打賞', 10, '2016-04-20')
要刪除一條數據,就先要找到這條數據,刪除代碼如下:
#region 4.0 刪除記錄
var toBeDeletedDonator = context.Donators.Single(d => d.Name == "待打賞");//根據Name找到要刪除的測試數據
if (toBeDeletedDonator!=null)
{
context.Donators.Remove(toBeDeletedDonator);//如果滿足條件,就將該對象使用Remove方法標記為Deleted
context.SaveChanges();//最后持久化到數據庫
}
#endregion
數據庫模式更改介紹
如果你修改了Donator類或者又添加了新的DbSet屬性(即添加了新表),在操作的過程中你可能會遇到一些異常。現在,我想再添加一張表PayWays,用來存儲打賞者的打賞方式,比如微信,支付寶,QQ紅包等。
先定義兩個字段,以后可能會增加字段和Donator表關聯:
namespace FirstCodeFirstApp
{
public class PayWay
{
public int Id { get; set; }
public string Name { get; set; }
}
}
在Context類中添加這句代碼public DbSet<PayWay> PayWays { get; set; }。
我們這里更加明顯地可以看到,數據庫上下文代表了整個數據庫,它包含多個表,每張表都成為了Context類的一個屬性。
現在,如果我們運行程序,並循環枚舉支付方式會出現什么情況呢?
啊哦!拋異常了:The model backing the 'Context' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).
意思是:自從數據庫創建以來,模型背后的數據庫上下文‘Context’已經發生了變化,也就是當初的數據庫和現在的上下文對不上了。呵呵!當然對不上了,數據庫上下文被我更改了,而數據庫沒改啊!

在后面我會介紹數據庫遷移和對已存在的數據庫進行遷移,但是我們想在也要解決這個當前問題。這就引入了初始化器(Initializer)的概念。初始化器會在首次實例化過程期間或者EF首次訪問數據庫時運行。EF中需要關心的初始化器有三種:
CreateDatabaseIfNotExists<TContext>DropCreateDatabaseIfModelChanges<TContext>DropCreateDatabaseAlways<TContext>
這兩種初始化器看其名字都很好理解,CreateDatabaseIfNotExists<TContext>指如果數據庫不存在則創建,DropCreateDatabaseIfModelChanges<TContext>指如果模型改變了(包括模型類的更改以及上下文中集合屬性的添加和移除)就銷毀之前的數據庫再創建數據庫,DropCreateDatabaseAlways<TContext>總是銷毀再重新創建數據庫,如果沒有指定的話默認使用第一個初始化器。
我們使用第二個來創建初始化器:
namespace FirstCodeFirstApp
{
public class Initializer:DropCreateDatabaseIfModelChanges<Context>
{
}
}
要使用該初始化器,我們應該在實例化數據庫上下文之前就要讓EF知道。我們可以使用EF的API中Database類的SetInitializer靜態方法。在我們的控制台應用中,我們應該將它放在Main方法的第一行:
static void Main(string[] args)
{
Database.SetInitializer(new Initializer());
//more code
}
如果我們現在運行程序,則不會拋異常了,而且數據庫結構也發生變化了。
如果數據庫在其他的應用程序中是打開的,如SQL Server Management Studio,那么會拋出一個不同的異常,會通知你EF無法獲得一個獨占的鎖定以銷毀數據庫。此時,只要關閉其他的應用程序就可以了。
未經處理的異常: System.Data.SqlClient.SqlException: 無法刪除數據庫 "CodeFirstApp",因為該數據庫當前正在使用。

需要注意的是,因為上面的初始化器會銷毀之前的數據庫,因此之前累積的所有數據也都丟失了。很顯然,這種用法不適合生產環境,但是我們學習EF或者項目早期是很方便的。另外一個有趣的功能是,初始化器允許我們在目標數據庫創建之后運行其他代碼,可以通過重寫Seed方法即可。該方法需要一個數據庫上下文的實例參數:
public class Initializer:DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
context.PayWays.AddRange(new List<PayWay>
{
new PayWay{Name = "支付寶"},
new PayWay{Name = "微信"},
new PayWay{Name = "QQ紅包"}
});
}
}
可以看到,在Seed方法中,我們不需要調用SaveChanges方法。此外,要更新生產數據庫,我們可以使用EF Migrations。

如上圖,Donators表中的數據都銷毀了,而Seed方法給PayWays表中添加了數據。
本章小結
這篇博客中,我們創建了第一個基於EF Code First的控制台應用程序。我們使用Nuget添加了EF的引用。然后我們確定要將贊助樓主的數據存儲在數據庫中,然后創建了一個Donator類映射到數據庫中的Donators表。然后創建了數據庫抽象Context類,它繼承自DbContext,並在它的構造函數中指定了想要的連接字符串,同時把該連接字符串添加到應用的配置文件中。然后,我們給數據庫上下文添加了一個屬性Donators,它是Donator的集合,即DbSet
自我測試
-
Dbcontext集合屬性的內部中的哪個基類可以用來表示數據庫中的一張表?
- List《T》
- DbSet《T》
- ICollection《T》
-
使用DbContext之后不必調用Dispose方法,對嗎?
-
EF中哪一個方法可以使用主鍵定位到數據庫中的一行?
- Find
- Locate
- Define
-
在找到一個記錄時,你可以使用DbSet的哪一個方法來刪除它?
- Delete
- Remove
- Erase
-
在EF中,如何輕松地更新一個人的名字?
- 執行SQL命令
- 獲得相應的對象,設置Name屬性,調用SaveChanges
- 其他
-
你給已經映射到數據庫中的模型類添加了屬性,如果你將數據庫初始化器設置為null,然后運行程序會發生什么?
- 拋異常
- 數據庫會更新到匹配的新模式,但是數據丟失了
注意注意:如果您覺得這篇文章對您有價值或者有所收獲,請點擊右下方的店長推薦,然后查看答案,謝謝!
參考書籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》
