8天掌握EF的Code First開發系列之5 視圖、存儲過程和異步API

本文出自8天掌握EF的Code First開發系列,通過本身的實踐整理出來。html


本篇目錄web

  • 視圖View
  • 存儲過程
  • 異步API
  • 本章小結

我們接着上一篇繼續深刻學習,這一篇說說Entity Framework之Code First方式如何使用視圖,存儲過程以及EF提供的一些異步接口。咱們會看到如何充分使用已存在的存儲過程和函數來檢索、修改數據。此外,咱們還要理解異步處理的優點以及EF是如何經過內置的API來支持這些概念的。sql

視圖View數據庫

視圖在RDBMS中扮演了一個重要的角色,它是將多個表的數據聯結成一種看起來像是一張表的結構,可是沒有提供持久化。所以,能夠將視圖當作是一個原生表數據頂層的一個抽象。例如,咱們可使用視圖提供不一樣安全的級別,也能夠簡化必須編寫的查詢,尤爲是咱們能夠在代碼中的多個地方頻繁地訪問使用視圖定義的數據。EF Code First如今還不徹底支持視圖,所以咱們必須使用一種變通方法。這種方法就是將視圖真正當作是一張表,讓EF定義這張表,而後再刪除它,最後再建立一個代替它的視圖。下面具體看看是如何實現的吧。後端

建立一個控制檯項目,取名「ViewsAndStoreProcedure」。數組

一、建立實體類安全

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; }

}

三、爲模擬視圖類建立配置類async

下面的代碼指定了主鍵和表名(也是視圖的名字,注意這裏的表名必定要和建立視圖的語句中的視圖名一致):

public class DonatorViewInfoMap : EntityTypeConfiguration<DonatorViewInfo>
{
    public DonatorViewInfoMap()
    {
        HasKey(m => m.DonatorId).ToTable("DonatorViews");
    }
}

四、上下文中添加模擬視圖類和配置類

web.config文件中的鏈接字符串我已配置好,不在此處展現!

public class DonatorContext  :DbContext
{
    public DonatorContext()
        : base("name = EFCodeFirst")
    {

    }

    public virtual DbSet<DonatorViewInfo> DonatorViews { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new DonatorViewInfoMap());

        base.OnModelCreating(modelBuilder);
    }
}

五、建立初始化器

public class Initializer : DropCreateDatabaseAlways<DonatorContext>
{
    protected override void Seed(DonatorContext context)
    {
        string drop = @"DROP TABLE DonatorViews";

        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.Donators.Province_Id = dbo.Provinces.Id ";

        context.Database.ExecuteSqlCommand(createView);

        base.Seed(context);
    }
}

上面的代碼中,咱們先使用Database對象的ExecuteSqlCommand方法銷燬生成的表,而後又調用該方法建立了咱們的視圖。該方法在容許開發者對後端執行任意的SQL代碼時頗有用。

上面的代碼寫完以後,在Main方法中只要寫這一句代碼Database.SetInitializer(new Initializer());,運行程序,就會看到數據庫中已經生成了Donators和Provinces兩張表和一個視圖DonatorView,見下圖:

 

若是在運行程序時出現以下異常:EF code first - Model compatibility cannot be checked because the database does not contain model metadata,那麼要將DropCreateDatabaseIfModelChanges修改成DropCreateDatabaseAlways便可

剛纔新建的數據庫是沒有數據的,而後咱們插入數據,在數據庫中查詢一下,能夠看到視圖中已經存在數據了:

 

下面,一切工做準備就緒,就能夠開始查詢數據了:

 static void Main(string[] args)
 {
     Database.SetInitializer(new Initializer());

     using (var context = new DonatorContext())
     {
         foreach (var donator in context.DonatorViews)
         {
             Console.WriteLine(donator.ProvinceName + "\t" + donator.DonatorId + "\t" + donator.DonatorName + "\t" + donator.Amount + "\t" + donator.DonateDate);
         }
     }

     Console.WriteLine("Finished");

     Console.ReadKey();
 }

執行結果以下圖所示:

 

正如上面的代碼所示,訪問視圖和任何數據表在代碼層面沒有區別,須要注意的地方就是在Seed方法中定義的視圖名稱要和定義的表名稱一致,不然就會由於找不到表對象而報錯,這一點要格外注意。

雖然視圖看起來很像一張表,可是若是咱們嘗試修改或更新視圖中定義的實體,那麼就會拋異常。

另外一種方法

若是咱們不想這麼折騰(先定義一張表,而後刪除這張表,再定義視圖),固然了,咱們仍是要在初始化器中定義視圖,可是咱們使用Database對象的另外一個方法SqlQuery查詢數據。該方法和ExecuteSqlCommand方法有相同的形參,可是最終返回一個結果集,在咱們這裏例子中,返回的就是DonatorViewInfo集合對象,以下代碼所示:

static void Main(string[] args)
{
    Database.SetInitializer(new Initializer());

    using (var context = new DonatorContext())
    {
        string sql = "SELECT DonatorId ,DonatorName ,Amount ,DonateDate ,ProvinceName FROM dbo.DonatorViews WHERE ProvinceName = {0}";

        var donators = context.Database.SqlQuery<DonatorViewInfo>(sql, "廣東省");

        foreach (var donator in donators)
        {
            Console.WriteLine(donator.ProvinceName + "\t" + donator.DonatorId + "\t" + donator.DonatorName + "\t" + donator.Amount + "\t" + donator.DonateDate);
        }
    }

    Console.WriteLine("Finished");

    Console.ReadKey();
}

SqlQuery方法須要一個泛型類型參數,該參數定義了原生SQL命令執行以後,將查詢結果集物質化成何種類型的數據。該文本命令自己就是參數化的SQL。咱們須要使用參數來確保動態sql不是SQL注入的目標。SQL注入是惡意用戶經過提供特定的輸入值執行任意SQL代碼的過程。EF自己不是這些攻擊的目標。

咱們不只看到了如何在EF中使用視圖,並且看到了兩個頗有用的Database對象,SqlQuery和ExecuteSqlCommand方法。SqlQuery方法的泛型參數不必定非得是一個類,也能夠.Net的基本類型,如string或者int。

執行結果以下:

 

存儲過程

一、在EF中使用已存在的存儲過程

在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.Province_Id

    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方法讀取數據了(注意:在使用存儲過程前,先要在數據庫中執行存儲過程),以下所示:

 static void Main(string[] args)
 {
     Database.SetInitializer(new Initializer());

     using (var context = new DonatorContext())
     {
         string sql = "[dbo].[SelectDonators] {0}";

         var donators = context.Database.SqlQuery<DonatorFromStoreProcedure>(sql, "山東省");

         foreach (var donator in donators)
         {
             Console.WriteLine(donator.ProvinceName + "\t" + donator.Name + "\t" + donator.Amount + "\t" + donator.DonateDate);
         }
     }

     Console.WriteLine("Finished");

     Console.ReadKey();
 }

上面的代碼中,咱們指定了使用哪一個類讀取查詢的結果,建立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 Province_Id=2/*給河北省的打賞者名字前加個前綴,並將金額加上指定的數量*/

END

如今數據庫中執行該存儲過程,而後,要調用該存儲過程,咱們使用ExecuteSqlCommand方法。該方法會返回存儲過程或者其餘任何SQL語句影響的行數。若是你對這個返回值不感興趣,那麼你能夠不理它。下面小試牛刀一把:

static void Main(string[] args)
{
    Database.SetInitializer(new Initializer());

    using (var context = new DonatorContext())
    {
        string sql = "[dbo].[UpdateDonator] {0}, {1}";

        context.Database.ExecuteSqlCommand(sql, "前綴", 2M);
    }

    Console.WriteLine("Finished");

    Console.ReadKey();
}

這裏咱們爲上面定義的存儲過程提供了兩個參數,一個是在每一個打賞者的姓名前加個前綴,另外一個是將打賞金額加2。這裏須要注意的是,咱們必須嚴格按照它們在存儲過程當中定義的順序依次傳入相應的值,它們會以參數數組傳入ExecuteSqlCommand。執行結果以下:

 

很大程度上,EF下降了存儲過程的須要,然而,仍舊有不少緣由要使用它們。這些緣由包括安全標準,遺留數據庫或者效率等問題。好比,若是須要在單個操做中更新幾千條數據,而後再經過EF檢索出來;若是每次都更新一行,而後再保存那些實例,效率是很低的。開發者能夠執行任意的SQL語句,只須要將上面SqlQuery或ExecuteSqlCommand方法中的存儲過程名稱改成要執行的SQL語句就能夠了。

二、使用 MapToStoredProcedures 生成存儲過程

至今,咱們都是使用EF內置的功能生成插入,更新或者刪除實體的SQL語句,總有某種緣由使咱們想使用存儲過程來實現相同的結果。開發者可能會爲了安全緣由使用存儲過程,也多是要處理一個已存在的數據庫,而這些存儲過程已經內置到該數據庫了。

EF Code First全面支持這些查詢。咱們可使用熟悉的EntityTypeConfiguration類來給存儲過程配置該支持,只須要簡單地調用MapToStoredProcedures方法就能夠了。若是咱們讓EF管理數據庫結構,那麼它會自動爲咱們生成存儲過程。此外,咱們還可使用MapToStoredProcedures方法合適的重載來重寫存儲過程名稱或者參數名。下面以donator類爲例:

class DonatorMap : EntityTypeConfiguration<Donator>
{
    public DonatorMap()
    {
        MapToStoredProcedures();
    }
}

若是咱們運行程序來建立或更新數據庫,就會看到爲咱們建立了新的存儲過程,默認爲插入操做生成了Donator_Insert,其餘的操做名稱相似,以下圖:

 

若是有必要的話,咱們能夠自定義存儲過程名,例如:

class DonatorMap : EntityTypeConfiguration<Donator>
{
    public DonatorMap()
    {
        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方法對一個查詢結果進行枚舉。

一、異步地從數據庫中獲取對象的列表

private static async Task<List<Donator>> GetDonatorsAsync()
{
    using (var context = new DonatorContext())
    {
        return await context.Donators.ToListAsync();
    }
}

值得注意的是,這裏使用了典型的async/await用法模式。函數被標記爲 async 並返回一個task對象,確切地說是一個Donator集合的task。而後,調用了DbContext的集合屬性建立了一個返回全部Donator的查詢。而後,使用ToListAsync擴展方法對該查詢結果進行枚舉。最後,因爲咱們須要遵照async/await模式,因此必須等待返回值。

任何EF查詢均可以使用ToListAsync或者ToArrayAsync轉換成異步版本。

二、異步建立一個新的對象

private static async Task InsertDonatorAsync(Donator donator)
{
    using (var context = new DonatorContext())
    {
        context.Donators.Add(donator);

        await context.SaveChangesAsync();
    }
}

代碼很簡單,和通常的同步模式比較,只是返回類型爲Task,方法多了async修飾,調用了SaveChangesAsync方法,同時注意,本身定義的方法最好也以Async後綴結尾,不是必須的,只是爲了遵照規範。

三、異步定位一條記錄

咱們能夠異步定位一條記錄,可使用不少方法,好比Single或First,這兩個方法都有異步版本。

private static async Task<Donator> FindDonatorAsync(int donatorId)
{
    using (var context = new DonatorContext())
    {
        return await context.Donators.FindAsync(donatorId);
    }
}

通常來講,就參數而言,EF中的全部異步方法和它們的同步副本都有相同的方法簽名。

四、異步聚合函數

對應於同步版本,異步聚合函數包括這麼幾個方法,MaxAsync、MinAsync、CountAsync、SumAsync、AverageAsync。

private static async Task<int> GetDonatorCountAsync()
{
    using (var context = new DonatorContext())
    {
        return await context.Donators.CountAsync();
    }
}

五、異步遍歷查詢結果

若是要對查詢結果進行異步遍歷,可使用ForEachAsync,能夠在任何查詢以後使用該方法。好比,下面將每一個打賞者的打賞日期設置爲今天。

private static async Task LoopDonatorsAsync()
{
    using (var db = new DonatorContext())
    {
        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就是一個好的例子。

相關文章
相關標籤/搜索