天天看點

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

  • 管理資料庫建立
    • 管理資料庫連接配接
    • 管理資料庫初始化
  • 填充種子資料
  • LINQ to Entities詳解
    • 什麼是LINQ to Entities
    • 使用LINQ to Entities操作實體
    • LINQ操作
    • 懶加載和預加載
    • 插入資料
    • 更新資料
    • 删除資料
  • 本章小結

本人的實驗環境是VS 2013 Update 5,windows 10,MSSQL Server 2008。

上一篇《Code First開發系列之領域模組化和管理實體關系》,我們主要介紹了EF中“約定大于配置”的概念,如何建立資料庫的表結構,以及如何管理實體間的三種關系和三種繼承模式。這一篇我們主要說三方面的問題,資料庫建立的管理,種子資料的填充以及CRUD的操作詳細用法。

1、管理資料庫連接配接

(1) 使用配置檔案管理連接配接

在資料庫上下文類中,如果我們隻繼承了無參數的DbContext,并且在配置檔案中建立了和資料庫上下文類同名的連接配接字元串,那麼EF會使用該連接配接字元串自動計算出該資料庫的位置和資料庫名。比如,我們的上下文定義如下:

public class SampleDbEntities : DbContext
    {
        
    }      

如果我們在配置檔案中定義的連接配接字元串如下:

<add name="SampleDbEntities" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=myTestDb;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\myTestDb.mdf" providerName="System.Data.SqlClient" />      

這樣,EF會使用該連接配接字元串執行資料庫操作。究竟發生了什麼呢?我們通過示例代碼來驗證一下。

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"時就會出現如下錯誤:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

解決方法如下:

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的話,那麼一旦上下文出了範圍,資料庫連接配接就會立即關閉。

2、管理資料庫初始化

首次運作EF Code First應用時,EF會做下面的這些事情:

  1. 檢查正在使用的

    DbContext

    類。
  2. 找到該上下文類使用的

    connectionString

  3. 找到領域實體并提取模式相關的資訊。
  4. 建立資料庫。
  5. 将資料插入系統。

一旦模式資訊提取出來,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來插入種子資料。

為了向資料庫插入一些初始化資料,我們需要建立滿足下列條件的資料庫初始化器類:

  • 從已存在的資料庫初始化器類中派生資料
  • 在資料庫建立期間種子化

1、定義領域實體

假設我們的資料模型Employer定義如下:

public class Employer
    {
        public int Id { get; set; }

        public string EmployerName { get; set; }
    }      

2、建立資料庫上下文

使用EF的Code First方法對上面的模型建立資料庫上下文:

public class SampleDbEntities : DbContext
    {
        public SampleDbEntities()
            : base("name = AppConnection")
        {
        }

        public DbSet<Employer> Employers { get; set; }
    }      

3、建立資料庫初始化器類

假設我們使用的是 

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()

,因為它會在基類中自動調用。

4、将資料庫初始化器類用于資料庫上下文類

public class SampleDbEntities : DbContext
    {
        public SampleDbEntities()
            : base("name = AppConnection")
        {
            Database.SetInitializer(new SeedingDataInitializer());
        }

        public DbSet<Employer> Employers { get; set; }
    }      

5、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();
        }      

6、運作程式,檢視效果

Main方法中隻是簡單的建立了資料庫上下文對象,然後将資料讀取出來:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

此外,我們可以從資料庫初始化的

Seed

方法中,通過資料庫上下文類給資料庫傳入原生SQL來影響資料庫模式。

到目前為止,我們已經學會了如何使用Code First方式來建立實體資料模型,也學會了使用EF進行領域模組化,執行模型驗證以及控制資料庫連接配接參數。一旦資料模組化完成,接下來就是要對這些模型進行各種操作了,通常有以下兩種方式:

  • LINQ to Entities
  • Entity SQL

本系列教程隻講LINQ to Entities,Entity SQL就是通過在EF中執行SQL,大家可以自行研究。

1、什麼是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對象。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

上圖展示了LINQ to Entities依賴

EntityClient

才能夠使用EF的概念資料模型,接下來我們看下LINQ to SQL如何執行該查詢并給應用程式傳回結果:

  1. 應用程式建立一個LINQ查詢。
  2. LINQ to Entities會将該LINQ查詢轉換成

    EntityClient

    指令。
  3. EntityClient

    指令然後使用EF和實體資料模型将這些指令轉換成SQL查詢。
  4. 然後會使用底層的ADO.NET provider将該SQL查詢傳入資料庫。
  5. 該查詢然後在資料庫中執行。
  6. 執行結果傳回給EF。
  7. EF然後将傳回的結果轉成CLR類型,比如領域實體。
  8. EntityClient

    使用項目,并傳回必要的結果給應用程式。

EntityClient

對象寄居在

System.Data.EntityClient

命名空間中,我們不必顯式建立該對象,我們隻需要使用命名空間,然後LINQ to Entities會處理剩下的事情。

如果我們對多種類型的資料庫使用LINQ to Entities,那麼我們隻需要為該資料庫使用正确的ADO.NET provider,然後

EntityClient

就會使用這個provider對任何資料庫的LINQ查詢無縫執行。

2、使用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查詢要求枚舉時才會執行。

3、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多餘的就不介紹了,看下圖,點選圖檔下載下傳并學習。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

下圖為LINQPad将linq文法轉換成了SQL。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

(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);      

最終的查詢結果都是一樣的:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

反過來,如果我們想要擷取打賞者“雪茄”的省份:

//查詢文法
 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);      

最終查詢的結果如下:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

生成的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();
        }
    }      

執行結果如下:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

當然,如果我們已經定義了一個包含了

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集合。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

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

升序查詢執行結果:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

隻要删除掉descending關鍵字就是升序排序了,預設排序方式是升序。

(7) 聚合操作

使用LINQ to Entities可以執行下面的聚合操作:

  • Count-數量
  • Sum-求和
  • Min-最小值
  • Max-最大值
  • Average-平均值

下面我找出山東省打賞者的數量:

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用括号括起來才能進行聚合(其實這是混合文法),沒有方法文法簡單靈活,是以下面的幾個方法我們隻用方法文法進行示範。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

其他聚合函數的代碼:

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天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

(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());
            }
        }      
8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

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();
        }      
8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解
和聚合函數一樣,分頁操作隻有方法文法。

(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

。無論右邊實體集合中是否包含任何實體,它總是會傳回左邊集合的所有元素。

4、懶加載和預加載

使用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

關鍵字即可。

(2) 預加載

預加載是這樣一種過程,當我們要加載查詢中的主要實體時,同時也加載與之相關的實體。要實作預加載,我們要使用

Include

方法。下面我們看一下如何在加載Donator資料的時候,同時也預先加載所有的Provinces資料:

var donators2 = context.Donators.Include(d => d.Province).ToList();
 var donators3 = context.Donators.Include("Provinces").ToList();      

這樣,當我們從資料庫中取到Donators集合時,也取到了Provinces集合。

5、插入資料

将新的資料插入資料庫有多種方法,可以使用之前的

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開始追蹤該實體以來,它的任何屬性都沒有改變。

執行結果如下,可見剛才添加的資料都插入資料庫了。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

6、更新資料

當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();
 }      
8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

我們需要手動處理的是要為每個發生變化的實體設定狀态。當然,如果要添加一個新的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根本就沒有追蹤,這樣就減少了開銷。

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

如果在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();
 }      

7、删除資料

删除和更新有很多相似之處,我們可以使用一個查詢找到資料,然後通過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());
        }
    }
}      
8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

上面的代碼會删除每個子實體,然後再删除根實體。删除一個實體時必須要知道它的主鍵值,上面的代碼删除了省份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);
}      

執行效果如下:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

毫無疑問你會發現删除操作非常不同于其他操作,要删除一個省份,我們隻需要傳入它的主鍵即可,要删除這個省份下的所有打賞者,我們隻需要在省份對象後追加要删除的打賞者對象,并給每個打賞者對象的Id屬性指派即可。在web應用中,我們需要送出所有的主鍵,或者需要查詢子記錄來找到對應的主鍵。

8、使用記憶體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);
     }
 }      

運作結果,可以看到追蹤到的狀态等:

8天掌握EF的Code First開發系列之3 管理資料庫建立,填充種子資料以及LINQ操作詳解

首先,我們看到了如何控制多個資料庫連接配接參數,如資料庫位置,資料庫名稱,模式等等,我們也看到了如何使用資料庫初始化器建立資料庫初始化政策以滿足應用程式的需求,最後,我們看到了如何在EF Code First中使用資料庫初始化器來插入種子資料。

接下來,我們看到了如何在EF中使用LINQ to Entities來查詢資料。我們看到了使用EF的LINQ to Entities無縫地執行各種資料檢索任務。最後,我們深入介紹了EF Code First中的插入、更新和删除!