本篇目錄
本系列的源碼本人已托管於coding上:點擊查看
先附上codeplex上EF的源碼:entityframework.codeplex.com,此外,本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。
咱們接着上一篇《Code First開發系列之管理數據庫創建,填充種子數據以及LINQ操作詳解》繼續深入學習,這一篇說說Entity Framework之Code First方式如何使用視圖,存儲過程以及EF提供的一些異步接口。我們會看到如何充分使用已存在的存儲過程和函數來檢索、修改數據。此外,我們還要理解異步處理的優勢以及EF是如何通過內置的API來支持這些概念的。
視圖View
視圖在RDBMS中扮演了一個重要的角色,它是將多個表的數據聯結成一種看起來像是一張表的結構,但是沒有提供持久化。因此,可以將視圖看成是一個原生表數據頂層的一個抽象。例如,我們可以使用視圖提供不同安全的級別,也可以簡化必須編寫的查詢,尤其是我們可以在代碼中的多個地方頻繁地訪問使用視圖定義的數據。EF Code First現在還不完全支持視圖,因此我們必須使用一種變通方法。這種方法就是將視圖真正看成是一張表,讓EF定義這張表,然后再刪除它,最后再創建一個代替它的視圖。下面具體看看是如何實現的吧。
創建一個控制台項目,取名“ViewsAndStoreProcedure”。
1 創建實體類
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; }
}
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 DonatorViewInfo
{
public int DonatorId { get; set; }
public string DonatorName { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
[StringLength(225)]
public string ProvinceName { get; set; }
}
為模擬視圖類創建配置類
下面的代碼指定了主鍵和表名(也是視圖的名字,注意這里的表名一定要和創建視圖的語句中的視圖名一致):
public class DonatorViewInfoMap:EntityTypeConfiguration<DonatorViewInfo>
{
public DonatorViewInfoMap()
{
HasKey(d => d.DonatorId);
ToTable("DonatorViews");
}
}
上下文中添加模擬視圖類和配置類
web.config文件中的連接字符串我已配置好,不在此處展示!
public class DonatorsContext : DbContext
{
public DonatorsContext()
: base("name=DonatorsConn")
{
}
public virtual DbSet<DonatorViewInfo> DonatorViews { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new DonatorViewInfoMap());
base.OnModelCreating(modelBuilder);
}
}
創建初始化器
public class Initializer:DropCreateDatabaseIfModelChanges<DonatorsContext>
{
protected override void Seed(DonatorsContext context)
{
var drop = "Drop Table DonatorView";
context.Database.ExecuteSqlCommand(drop);
var createView = @"CREATE VIEW [dbo].[DonatorViews]
AS SELECT
dbo.Donators.Id AS DonatorId,
dbo.Donators.Name AS DonatorName,
dbo.Donators.Amount AS Amount,
dbo.Donators.DonateDate AS DonateDate,
dbo.Provinces.ProvinceName AS ProvinceName
FROM dbo.Donators
INNER JOIN dbo.Provinces ON dbo.Provinces.Id = dbo.Donators.ProvinceId";
context.Database.ExecuteSqlCommand(createView);
base.Seed(context);
}
}
上面的代碼中,我們先使用Database
對象的ExecuteSqlCommand
方法銷毀生成的表,然后又調用該方法創建了我們的視圖。該方法在允許開發者對后端執行任意的SQL代碼時很有用。
上面的代碼寫完之后,在Main方法中只要寫這一句代碼 Database.SetInitializer(new Initializer());
,運行程序,就會看到數據庫中已經生成了Donators和Provinces兩張表和一個視圖DonatorView,見下圖:
剛才新建的數據庫是沒有數據的,然后我們插入數據,在數據庫中查詢一下,可以看到視圖中已經存在數據了:
下面,一切工作准備就緒,就可以開始查詢數據了:
#region 1.0 視圖
Database.SetInitializer(new Initializer());
using (var db = new DonatorsContext())
{
var donators = db.DonatorViews;
foreach (var donator in donators)
{
Console.WriteLine(donator.ProvinceName + "\t" + donator.DonatorId + "\t" + donator.DonatorName + "\t" + donator.Amount + "\t" + donator.DonateDate);
}
}
#endregion
執行結果如下圖所示:
正如上面的代碼所示,訪問視圖和任何數據表在代碼層面沒有區別,需要注意的地方就是在Seed方法中定義的視圖名稱要和定義的表名稱一致,否則就會因為找不到表對象而報錯,這一點要格外注意。
雖然視圖看起來很像一張表,但是如果我們嘗試修改或更新視圖中定義的實體,那么就會拋異常。
另一種方法
如果我們不想這么折騰(先定義一張表,然后刪除這張表,再定義視圖),當然了,我們還是要在初始化器中定義視圖,但是我們使用Database對象的另一個方法SqlQuery
查詢數據。該方法和ExecuteSqlCommand
方法有相同的形參,但是最終返回一個結果集,在我們這里例子中,返回的就是DonatorViewInfo集合對象,如下代碼所示:
//1.2 另一種方法
var sql = @"SELECT DonatorId ,DonatorName ,Amount ,DonateDate ,ProvinceName from dbo.DonatorViews where ProvinceName={0}";
var donatorsViaCommand = db.Database.SqlQuery<DonatorViewInfo>(sql,"河北省");
foreach (var donator in donatorsViaCommand)
{
Console.WriteLine(donator.ProvinceName + "\t" + donator.DonatorId + "\t" + donator.DonatorName + "\t" + donator.Amount + "\t" + donator.DonateDate);
}
SqlQuery
方法需要一個泛型類型參數,該參數定義了原生SQL命令執行之后,將查詢結果集物質化成何種類型的數據。該文本命令本身就是參數化的SQL。我們需要使用參數來確保動態sql不是SQL注入的目標。SQL注入是惡意用戶通過提供特定的輸入值執行任意SQL代碼的過程。EF本身不是這些攻擊的目標。
我們不僅看到了如何在EF中使用視圖,而且看到了兩個很有用的Database
對象,SqlQuery
和ExecuteSqlCommand
方法。SqlQuery
方法的泛型參數不一定非得是一個類,也可以.Net的基本類型,如string或者int。
執行結果如下:
存儲過程
在EF中使用存儲過程和使用視圖是很相似的,一般會使用Database
對象上的兩個方法——SqlQuery
和ExecuteSqlCommand
。為了從存儲過程中讀取很多數據行,我們只需要定義一個類,我們會將檢索到的所有數據行物質化到該類實例的集合中。比如,從下面的存儲過程讀取數據:
CREATE PROCEDURE SelectDonators
@provinceName AS NVARCHAR(10)
AS
BEGIN
SELECT ProvinceName,Name,Amount,DonateDate FROM dbo.Donators
JOIN dbo.Provinces ON dbo.Provinces.Id = dbo.Donators.ProvinceId
WHERE ProvinceName=@provinceName
END
我們只需要定義一個匹配了存儲過程結果的類(類的屬性名必須和表的列名一致)即可,如下所示:
public class DonatorFromStoreProcedure
{
public string ProvinceName { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
}
還是插入以下數據進行測試:
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)
現在我們就可以使用SqlQuery
方法讀取數據了(注意:在使用存儲過程前,先要在數據庫中執行存儲過程),如下所示:
#region 2.0 EF調用存儲過程查詢數據SqlQuery
using (var db=new DonatorsContext())
{
var sql = "SelectDonators {0}";
var donators = db.Database.SqlQuery<DonatorFromStoreProcedure>(sql,"山東省");
foreach (var donator in donators)
{
Console.WriteLine(donator.ProvinceName+"\t"+donator.Name+"\t"+donator.Amount+"\t"+donator.DonateDate);
}
}
#endregion
上面的代碼中,我們指定了使用哪個類讀取查詢的結果,創建SQL語句時,也為存儲過程的參數提供了一個格式化占位符,調用SqlQuery
時為那個參數提供了一個值。假如要提供多個參數的話,多個格式化占位符必須用逗號分隔,還要給SqlQuery
提供值的數組(后面會舉例)。我們也可以使用表值函數代替存儲過程。
存儲過程成功執行,結果如下:
另一個用例就是假如存儲過程沒有返回任何值,只是對數據庫中的一張或多張表執行了一條命令的情況。一個存儲過程干了多少事情不重要,重要的是它壓根不需要返回任何東西。例如,下面的存儲過程只是更新了一些東西:
CREATE PROCEDURE UpdateDonator
@namePrefix AS NVARCHAR(10),
@addedAmount AS DECIMAL
AS
BEGIN
UPDATE dbo.Donators SET Name=@namePrefix+Name,Amount=Amount+@addedAmount
WHERE ProvinceId=2/*給河北省的打賞者名字前加個前綴,並將金額加上指定的數量*/
END
現在數據庫中執行該存儲過程,然后,要調用該存儲過程,我們使用ExecuteSqlCommand
方法。該方法會返回存儲過程或者其他任何SQL語句影響的行數。如果你對這個返回值不感興趣,那么你可以不理它。下面小試牛刀一把:
//2.1 EF調用存儲過程之ExecuteSqlCommand方法
using (var db = new DonatorsContext())
{
var sql = "UpdateDonator {0},{1}";
Console.WriteLine("執行存儲過程前的數據為:");
PrintDonators();
var rowsAffected = db.Database.ExecuteSqlCommand(sql, "Update", 10m);
Console.WriteLine("影響的行數為{0}條", rowsAffected);
Console.WriteLine("執行存儲過程之后的數據為:");
PrintDonators();
}
static void PrintDonators()
{
using (var db = new DonatorsContext())
{
var donators = db.Donators.Where(p => p.ProvinceId == 2);//找出河北省的打賞者
foreach (var donator in donators)
{
Console.WriteLine(donator.Name + "\t" + donator.Amount + "\t" + donator.DonateDate);
}
}
}
這里我們為上面定義的存儲過程提供了兩個參數,一個是在每個打賞者的姓名前加個前綴“Update”,另一個是將打賞金額加10。這里需要注意的是,我們必須嚴格按照它們在存儲過程中定義的順序依次傳入相應的值,它們會以參數數組傳入ExecuteSqlCommand
。執行結果如下:
很大程度上,EF降低了存儲過程的需要,然而,仍舊有很多原因要使用它們。這些原因包括安全標准,遺留數據庫或者效率等問題。比如,如果需要在單個操作中更新幾千條數據,然后再通過EF檢索出來;如果每次都更新一行,然后再保存那些實例,效率是很低的。最后,即使你使用了SqlQuery
方法調用了存儲過程,也可以更新數據。
開發者可以執行任意的SQL語句,只需要將上面
SqlQuery
或ExecuteSqlCommand
方法中的存儲過程名稱改為要執行的SQL語句就可以了。
使用存儲過程CUD
至今,我們都是使用EF內置的功能生成插入,更新或者刪除實體的SQL語句,總有某種原因使我們想使用存儲過程來實現相同的結果。開發者可能會為了安全原因使用存儲過程,也可能是要處理一個已存在的數據庫,而這些存儲過程已經內置到該數據庫了。
EF Code First全面支持這些查詢。我們可以使用熟悉的EntityTypeConfiguration
類來給存儲過程配置該支持,只需要簡單地調用MapToStoredProcedures
方法就可以了。如果我們讓EF管理數據庫結構,那么它會自動為我們生成存儲過程。此外,我們還可以使用MapToStoredProcedures
方法合適的重載來重寫存儲過程名稱或者參數名。下面以donator類為例:
public class DonatorMap:EntityTypeConfiguration<Donator>
{
public DonatorMap()
{
MapToStoredProcedures();
}
}
如果我們運行程序來創建或更新數據庫,就會看到為我們創建了新的存儲過程,默認為插入操作生成了Donator_Insert
,其他的操作名稱類似,如下圖:
如果有必要的話,我們可以自定義存儲過程名,例如:
public class DonatorMap:EntityTypeConfiguration<Donator>
{
public DonatorMap()
{
//MapToStoredProcedures();
MapToStoredProcedures(config =>
{
//將刪除打賞者的默認存儲過程名稱更改為“DonatorDelete”,
//同時將該存儲過程的參數名稱更改為“donatorId”,並指定該值來自Id屬性
config.Delete(
procConfig =>
{
procConfig.HasName("DonatorDelete");
procConfig.Parameter(d => d.Id, "donatorId");
});
//將默認的插入存儲過程名稱更改為“DonatorInsert”
config.Insert(
procConfig =>
{
procConfig.HasName("DonatorInsert");
});
//將默認的更新存儲過程名稱更改為“DonatorUpdate”
config.Update(procConfig =>
{
procConfig.HasName("DonatorUpdate");
});
});
}
}
總之,要自定義的話,代碼肯定更冗余,不管怎樣了,取決於你!
異步API
目前為止,我們所有使用EF的數據庫操作都是同步的。換言之,我們的.NET程序會等待給定的數據庫操作(例如一個查詢或者一個更新)完成之后才會繼續向前執行。在很多情況下,使用這種方式沒有什么問題,然而,在某些情況下,異步地執行這些操作的能力是很重要的。在這些情況下,當該軟件等待數據庫操作完成時,我們讓.Net使用它的的執行線程。例如,如果使用了異步的方式在創建一個Web應用,當我們等待數據庫完成處理一個請求(無論它是一個保存還是檢索操作)時,通過將web工作線程釋放回線程池,就可以更有效地利用服務器資源。
即使在桌面應用中,異步API也很有用,因為用戶可能會潛在執行應用中的其他任務,而不是等待一個可能耗時的查詢或保存操作。換言之,.Net線程不需要等待數據庫線程完成跟數據庫有關的工作。在許多應用程序中,異步API沒有帶來好處,從性能的角度來說,甚至可能是有害的,因為線程上下文的切換開銷。因此,在使用異步API之前,開發者需要確定使用異步API會讓你受益!
EF暴露了很多異步操作,按照約定,所有的這些方法都以Async
后綴結尾。對於保存操作,我們可以使用DbContext
上的SaveChangesAsync
方法。也有很多查詢的方法,比如,許多聚合函數都有異步副本,比如SumAsync
和AverageAsync
。還可以使用ToListAsync
和ToArrayAsync
將一個結果集讀入到一個list或者array中。此外,還可以使用ForEachAsync
方法對一個查詢結果進行枚舉。
異步地從數據庫中獲取對象的列表
#region 3.0 異步API
//3.1 異步查詢對象列表
static async Task<IEnumerable<Donator>> GetDonatorsAsync()
{
using (var db = new DonatorsContext())
{
return await db.Donators.ToListAsync();
}
}
#endregion
值得注意的是,這里使用了典型的async/await用法模式。函數被標記為 async並返回一個task對象,確切地說是一個Donator集合的task。然后,調用了DbContext的集合屬性創建了一個返回所有Donator的查詢。然后,使用ToListAsync
擴展方法對該查詢結果進行枚舉。最后,由於我們需要遵守async/await模式,所以必須等待返回值。
任何EF查詢都可以使用
ToListAsync
或者ToArrayAsync
轉換成異步版本。
異步創建一個新的對象
//3.2 異步創建一個新的對象
static async Task InsertDonatorAsync(Donator donator)
{
using (var db = new DonatorsContext())
{
db.Donators.Add(donator);
await db.SaveChangesAsync();
}
}
代碼很簡單,和一般的同步模式比較,只是返回類型為Task,方法多了async修飾,調用了SaveChangesAsync方法,同時注意,自己定義的方法最好也以Async后綴結尾,不是必須的,只是為了遵守規范。
異步定位一條記錄
我們可以異步定位一條記錄,可以使用很多方法,比如Single
或First
,這兩個方法都有異步版本。
//3.3 異步定位一條記錄
static async Task<Donator> FindDonatorAsync(int donatorId)
{
using (var db = new DonatorsContext())
{
return await db.Donators.FindAsync(donatorId);
}
}
一般來說,就參數而言,EF中的所有異步方法和它們的同步副本都有相同的方法簽名。
異步聚合函數
對應於同步版本,異步聚合函數包括這么幾個方法,MaxAsync
,MinAsync
,CountAsync
,SumAsync
,AverageAsync
。
//3.4 異步聚合函數
static async Task<int> GetDonatorCountAsync()
{
using (var db = new DonatorsContext())
{
return await db.Donators.CountAsync();
}
}
異步遍歷查詢結果
如果要對查詢結果進行異步遍歷,可以使用ForEachAsync
,可以在任何查詢之后使用該方法。比如,下面將每個打賞者的打賞日期設置為今天。
//3.5 異步遍歷查詢結果
static async Task LoopDonatorsAsync()
{
using (var db = new DonatorsContext())
{
await db.Donators.ForEachAsync(d =>
{
d.DonateDate=DateTime.Today;
});
}
}
如果要在一個同步方法中使用一個異步方法,那么我們可以使用Task的API等待一個任務完成。比如,我們可以訪問task的Result屬性,這會造成當前的線程暫停並且讓該task完成執行,但一般不建議這么做,最佳實踐是總是使用async。
同步方法中調用異步方法的代碼如下:
Console.WriteLine(FindDonatorAsync(1).Result.DonateDate);
上面這句代碼在Main方法中,調用了之前定義的異步方法,然后訪問了該Task的Result屬性,這會造成異步函數完成執行。
當決定是否使用異步API的時候,首先要研究一下,並確定為什么要使用異步API。既然用了異步API,為了獲得最大的編碼好處,就要確保整個方法的調用連都是異步的。最后,當需要時在使用Task API。
本章小結
EF給開發者帶來了很大價值,允許我們使用C#代碼管理數據庫數據。然而,有時我們需要通過動態的SQL語句或者存儲過程,更直接地對視圖訪問數據,就可以使用ExecuteSqlCommand
方法來執行任意的SQL代碼,包括原生SQL或者存儲過程。也可以使用SqlQuery
方法從視圖、存儲過程或任何SQL語句中檢索數據,EF會基於我們提供的結果類型物質化查詢結果。當給這兩個方法提供參數時,避免SQL注入漏洞很重要。
EF也可以自動為實體生成插入、更新和刪除的存儲過程,假如你對這些存儲過程的命名規范和編碼標准滿意的話,我們只需要在配置伙伴類中寫一行代碼就可以了。
EF也提供了異步操作支持,包括查詢和更新。為了避免潛在的性能影響,開發者使用這些技術時務必謹慎。在某些技術中,異步API很適合,Web API就是一個好的例子。
自我測試
- EF不能從視圖獲取數據,對嗎?
- 哪個數據庫方法可以使用SQL語句查詢和檢索數據?
- ExecuteSqlCommand
- Execute
- SqlQuery
- 如果你想為一個實體類型的CRUD操作映射到一組存儲過程,就必須手動寫所有的SQL Server存儲過程,對嗎?
- 使用異步API沒有負面影響,對嗎?
DbContext
中用於異步保存更改的是什么方法?
如果您覺得這篇文章對您有價值或者有所收獲,請點擊右下方的店長推薦,謝謝!
參考書籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》