Entity Framework Core 2.0 入門

weixin_34221276發表於2018-03-12

該文章比較基礎, 不多說廢話了, 直接切入正題.

該文分以下幾點:

  • 建立Model和資料庫
  • 使用Model與資料庫互動
  • 查詢和儲存關聯資料

EF Core支援情況

EF Core的資料庫Providers:

此外還即將支援CosmosDB和 Oracle.

EFCore 2.0新的東西:

查詢:

  • EF.Functions.Like()
  • Linq直譯器的改進
  • 全域性過濾(按型別)
  • 編譯查詢(Explicitly compiled query)
  • GroupJoin的SQL優化.

 對映:

  • Type Configuration 配置
  • Owned Entities (替代EF6的複雜型別)
  • Scalar UDF對映
  • 分表

效能和其他

  • DbContext Pooling, 這個很好
  • Raw SQL插入字串.
  • Logging
  • 更容易定製配置

1.建立資料庫和Model

準備.net core專案

專案結構如圖:

由於我使用的是VSCode, 所以需要使用命令列:

mkdir LearnEf && cd LearnEf
dotnet new sln // 建立解決方案

mkdir LearnEf.Domains && cd LearnEf.Domains
dotnet new classlib // 建立LearnEf.Domains專案

cd ..
mkdir LearnEf.Data && cd LearnEf.Data
dotnet new classlib // 建立LearnEf.Data專案

cd ..
mkdir LearnEf.UI && cd LearnEf.UI
dotnet new console // 建立控制檯專案

cd ..
mkdir LearnEf.Tests && cd LearnEf.Tests
dotnet new xunit // 建立測試專案

為解決方案新增專案:

dotnet sln add LearnEf.UI/LearnEf.UI.csproj
dotnet sln add LearnEf.Domains/LearnEf.Domains.csproj
dotnet sln add LearnEf.Data/LearnEf.Data.csproj
dotnet sln add LearnEf.Tests/LearnEf.Tests.csproj

 

為專案之間新增引用:

LearnEf.Data依賴LearnEf.Domains:

cd LearnEf.Data
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj

 

LearnEf.Console依賴LearnEf.Domains和LearnEf.Data:

cd ../LearnEf.UI
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

LearnEf.Test依賴其它三個專案:

cd ../LearnEf.Tests
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj ../LearnEf.UI/LearnEf.UI.csproj

 

(可能需要執行dotnet restore)

在Domains專案下直接建立兩個Model, 典型的一對多關係Company和Department:

using System;
using System.Collections.Generic;

namespace LearnEf.Domains
{
    public class Company
    {
        public Company()
        {
            Departments = new List<Department>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime StartDate { get; set; }
        public List<Department> Departments { get; set; }
    }
}
namespace LearnEf.Domains
{
    public class Department
    {
        public int Id { get; set; }
        public int CompanyId { get; set; }
        public Company Company { get; set; }
    }
}

 

新增Entity Framework Core庫:

首先Data專案肯定需要安裝這個庫, 而我要使用sql server, 參照官方文件, 直接在解決方案下執行這個命令:

dotnet add ./LearnEf.Data package Microsoft.EntityFrameworkCore.SqlServer
dotnet restore

 

建立DbContext:

在Data專案下建立MyContext.cs:

using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
    }
}

指定資料庫Provider和Connection String:

在EFCore裡, 必須明確指定Data Provider和Connection String.

可以在Context裡面override這個Onconfiguring方法:

有一個錯誤, 應該是Server=localhost;

(這裡無需呼叫父類的方法, 因為父類的方法什麼也沒做).

UseSqlServer表示使用Sql Server作為Data Provider. 其引數就是Connection String.

在執行時EfCore第一次例項化MyContext的時候, 就會觸發這個OnConfiguring方法. 此外, Efcore的遷移Api也可以獲得該方法內的資訊.

EF Core遷移:

簡單的來說就是 Model變化 --> 建立migration檔案 --> 應用Migration到資料庫或生成執行指令碼.

新增Migration (遷移):

由於我使用的是VSCode+dotnet cli的方法, 所以需要額外的步驟來使dotnet ef命令可用.

可以先試一下現在的效果:

可以看到, dotnet ef 命令還不可用.

所以參考官方文件: https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

可執行專案(Startup project)需要EFCore遷移引擎庫, 所以對LearnEf.UI新增這個庫:

dotnet add ./LearnEf.UI package Microsoft.EntityFrameworkCore.Design
dotnet restore

 

然後開啟LearnEf.UI.csproj 新增這段程式碼, 這個庫是EF的命令庫:

 <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  </ItemGroup>

 

最後內容如下:

然後再執行dotnet ef命令, 就應該可用了:

現在, 新增第一個遷移:

cd LearnEf.UI
dotnet ef migrations add Initial --project=../LearnEf.Data

 

--project引數是表示需要使用的專案是哪個.

命令執行後, 可以看到Data專案生成了Migrations目錄和一套遷移檔案和一個快照檔案:

檢查這個Migration.

前邊帶時間戳的那兩個檔案是遷移檔案.

另一個是快照檔案, EFCore Migrations用它來跟蹤所有Models的當前狀態. 這個檔案非常重要, 因為下次你新增遷移的時候, EFcore將會讀取這個快照並將它和Model的最新版本做比較, 就這樣它就知道哪些地方需要有變化.

這個快照檔案解決了老版本Entity Framework的一個頑固的團隊問題.

使用遷移檔案建立指令碼或直接生成資料庫.

生成建立資料庫的SQL指令碼:

dotnet ef migrations script --project=../LearnEf.Data/LearnEf.Data.csproj

 

Sql指令碼直接列印在了Command Prompt裡面. 也可以通過指定--output引數來輸出到具體的檔案.

這裡, 常規的做法是, 針對開發時的資料庫, 可以通過命令直接建立和更新資料庫. 而針對生產環境, 最好是生成sql指令碼, 然後由相關人員去執行這個指令碼來完成資料庫的建立或者更新.

直接建立資料庫:

dotnet ef database update --project=../LearnEf.Data/LearnEf.Data.csproj --verbose

 

--verbose表示顯示執行的詳細過程, 其結果差不多這樣:

這裡的執行過程和邏輯是這樣的: 如果資料庫不存在, 那麼efcore會在指定的連線字串的地方建立該資料庫, 並應用當前的遷移. 如果是生成的sql指令碼的話, 那麼這些動作必須由您自己來完成.

然後檢視一下生成的表. 

不過首先, 如果您也和我一樣, 沒有裝Sql server management studio或者 Visual Studio的話, 請您先安裝VSCode的mssql這個擴充套件:

重啟後, 建立一個Sql資料夾, 然後建立一個Tables.sql檔案, 開啟命令皮膚(windows: Shift+Ctrl+P, mac: Cmd+Shift+P), 選擇MS SQL: Connect.

然後選擇Create Connection Profile:

輸入Sql的伺服器地址:

再輸入資料庫名字:

選擇Sql Login(我使用的是Docker, 如果windows的話, 可能使用Integrated也可以):

輸入使用者名稱:

密碼:

選擇是否儲存密碼:

最後輸入檔案的名字:

隨後VSCode將嘗試連線該資料庫, 成功後右下角會這樣顯示 (我這裡輸入有一個錯誤, 資料庫名字應該是LearnEF):

隨後在該檔案中輸入下面這個sql語句來查詢所有的Table:

--  Table 列表
SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE='BASE TABLE';

 

執行sql的快捷鍵是windows: Shift+Ctrp+E, mac: Cmd+Shift+E, 或者滑鼠右鍵.

結果如圖:

OK表是建立成功了(還有一個遷移歷史表, 這個您應該知道).

接下來我看看錶的定義:

-- Companies表:
exec sp_help 'Companies';

 

其中Name欄位是可空的並且長度是-1也就是nvarchar(Max).

Departments表的Name欄位也是一樣的.

再看看那個MigrationHistory表:

-- MigrationHistory:
SELECT * FROM dbo.__EFMigrationsHistory;

可以看到, efcore到migration 歷史表裡面只儲存了MigrationId.

在老版本到ef裡, migration歷史表裡面還儲存著當時到遷移的快照, 建立遷移的時候還需要與資料庫打交道. 這就是我上面提到的如果團隊使用ef和原始碼管理的話, 就會遇到這個非常令人頭疼的問題.

如果使用asp.net core的話.

在解決方案裡再建立一個asp.net core mvc專案:

mkdir LearnEf.Web && cd LearnEf.Web
dotnet new mvc

 

在解決方案裡新增該專案:

dotnet sln add ./LearnEf.Web/LearnEf.Web.csproj

 

為該專案新增必要的引用:

cd LearnEf.Web
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

為測試專案新增該專案引用:

cd ../*Tests
dotnet add reference ../LearnEf.Web/LearnEf.Web.csproj

 

操作完之後, 我們可以做以下調整, 去掉MyContext裡面的OnConfiguring方法, 因為asp.net core有內建的依賴注入機制, 我可以把已經構建好的DbContextOptions直接注入到建構函式裡:

這樣的話, 我們可以讓asp.net core來決定到底使用哪個Data Provider和Connection String:

這也就意味著, Web專案需要引用EfCore和Sql Provider等, 但是不需要, 因為asp.net core 2.0這個專案模版引用了AspNetCore.All這個megapack, 裡面都有這些東西了.

雖然這個包什麼都有, 也就是說很大, 但是如果您使用Visual Studio Tooling去部署的話, 那麼它只會部署那些專案真正用到的包, 並不是所有的包.

接下來, 在Web專案的Startup新增EfCore相關的配置:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;"));
        }

 

這句話就是把MyContext註冊到了asp.net core的服務容器中, 可以供注入, 同時在這裡指定了Data Provider和Connection String.

與其把Connection String寫死在這裡, 不如使用appSettings.json檔案:

然後使用內建的方法讀取該Connection String:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        }

 

回到命令列進入Web專案, 使用dotnet ef命令:

說明需要新增上面提到的庫, 這裡就不重複了.

然後, 手動新增一個Migration叫做InitialAspNetCore:

dotnet ef migrations add InitialAspNetCore --project=../LearnEf.Data

 

看一下遷移檔案:

是空的, 因為我之前已經使用UI那個專案進行過遷移更新了. 所以我要把這個遷移刪掉:

dotnet ef migrations remove --project=../LearnEf.Data

 

然後這兩個遷移檔案就刪掉了:

多對多關係和一對一關係:

這部分的官方文件在這: https://docs.microsoft.com/en-us/ef/core/modeling/relationships

對於多對多關係, efcore需要使用一箇中間表, 我想基本ef使用者都知道這個了, 我就直接貼程式碼吧.

建立一個City.cs:

namespace LearnEf.Domains
{
    public class City
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

 

Company和City是多對多的關係, 所以需要建立一箇中間表,叫做 CompanyCity:

namespace LearnEf.Domains
{
    public class CompanyCity
    {
        public int CompanyId { get; set; }
        public int CityId { get; set; }
        public Company Company { get; set; }
        public City City { get; set; }
    }
}

 

修改Company:

修改City:

儘管Efcore可以推斷出來這個多對多關係, 但是我還是使用一下FluentApi來自定義配置一下這個表的主鍵:

MyContext.cs:

using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options)
            : base(options)
        {

        }
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<CompanyCity> CompanyCities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });
        }

        // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        // {
        //     optionsBuilder.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;");
        //     base.OnConfiguring(optionsBuilder);
        // }
    }
}

 

完整的寫法應該是:

其中紅框裡面的部分不寫也行.

接下來建立一個一對一關係, 建立Model叫Owner.cs:

namespace LearnEf.Domains
{
    public class Owner
    {
public int Id { get; set;}
public int CompanyId { get; set; } public string Name { get; set; } public Company Company { get; set; } } }

 

修改Company:

配置關係:

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.Company)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CompanyId);

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.City)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CityId);

            modelBuilder.Entity<Owner>().HasOne(x => x.Company).WithOne(x => x.Owner)
                .HasForeignKey<Owner>(x => x.CompanyId);
        }

 

 

這裡面呢, 這個Owner對於Company 來說 是可空的. 而對於Owner來說, Company是必須的. 如果針對Owner想讓Company是可空的, 那麼CompanyId的型別就應該設定成int?.

再新增一個遷移:

dotnet ef migrations add AddRelationships --project=../LearnEf.Data

 

檢視遷移檔案:

檢視一下快照;

沒問題, 那麼更新資料庫:

dotnet ef database update AddRelationships --project=../LearnEf.Data --verbose

 

更新成功:

對現有資料庫的反向工程

這部分請檢視官方文件吧, 很簡單, 我實驗了幾次, 但是目前還沒有這個需求.

使用Model與資料庫互動

輸出Sql語句.

對於asp.net core 2.0專案, 參考官方文件: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?tabs=aspnetcore2x

實際上, 專案已經配置好Logging部分了, 預設是列印到控制檯和Debug視窗的. 原始碼: https://github.com/aspnet/MetaPackages/blob/dev/src/Microsoft.AspNetCore/WebHost.cs

而對於console專案, 文件在這: https://docs.microsoft.com/en-us/ef/core/miscellaneous/logging

需要對LearnEf.Data專案新增這個包: 

cd LearnEf.Data
dotnet add package Microsoft.Extensions.Logging.Console
dotnet restore

 

然後為了使用console專案, 需要把MyContext改回來:

這部分首先是使用LoggerFactory建立了一個特殊的Console Logger. .net core的logging可以顯示很多的資訊, 這裡我放置了兩個過濾: 第一個表示只顯示Sql命令, 第二個表示細節的顯示程度是Information級別.

最後還要在OnConfiguring方法裡告訴modelBuilder使用MyLoggerFactory作為LoggerFactory.

這就配置好了.

插入資料.

這部分很簡單, 開啟UI專案的Program.cs:

這裡都懂的, 建立好model之後, 新增到context的DbSet屬性裡, 這時context就開始追蹤這個model了.

SaveChanges方法, 會檢查所有被追蹤的models, 讀取他們的狀態. 這裡用到是Add方法, context就會知道這個model的狀態是new, 所以就應該被插入到資料庫. 然後它就根據配置會生成出相應的sql語句, 然後把這個SQL語句執行到資料庫. 如果有返回資料的話, 就取得該資料.

下面就執行一下這個console程式:

dotnet run --project=./LearnEf.UI

 

看下控制檯:

可以看到輸出了sql語句, 而且這個出入動作後, 做了一個查詢把插入資料生成的Id取了回來.

預設情況下log不顯示傳進去的引數, 這是為了安全. 但是可以通過修改配置來顯示引數:

然後控制檯就會顯示這些引數了:

批量插入操作.

可以使用AddRange新增多條資料. 其引數可以是params或者集合.

可以看到這個和之前Add的Sql語句是完全不同的:

這個語句我不是很明白.

批量新增不同型別的資料:

使用context的AddRange或Add方法, DbContext可以推斷出引數的型別, 並執行正確的操作. 上面的方法就是使用了DbContext.AddRange方法, 一次性新增了兩種不同型別的model.

這兩個方法對於寫一些通用方法或者處理複雜的情況是很有用的.

Sql Server對於批量操作的限制是, 一次只能最多處理1000個SQL命令, 多出來的命令將會分批執行.

如果想更改這個限制, 可以這樣配置引數:

簡單查詢.

針對DbSet, 使用Linq的ToList方法, 會觸發對資料庫對查詢操作:

首先把Company的ToString方法寫上:

這樣方便輸入到控制檯.

然後寫查詢方法:

看結果:

EfCore到查詢有兩類語法, 一種是Linq方法, 另一種是Linq查詢語法:

這種是Linq方法:

下面這種是Linq查詢語法:

我基本都是使用第一種方法.

除了ToList(Async)可以觸發查詢以外, 遍歷foreach也可以觸發查詢:

但是這種情況下, 可能會有效能問題. 因為:

在遍歷開始的時候, 資料庫連線開啟, 並且會一直保持開啟的狀態, 直到遍歷結束.

所以如果這個遍歷很耗時, 那麼可能會發生一些問題.

最好的辦法還是首先執行ToList, 然後再遍歷.

查詢的過濾.

這部分和以前的EF基本沒啥變化.

這個很簡單, 不說了.

這裡列一下可觸發查詢的Linq方法:

還有個兩個方法是DbSet的方法, 也可以觸發查詢動作:

上面這些方法都應該很熟悉, 我就不寫了.

過濾的條件可以直接家在上面的某些方法裡面, 例如:

通過主鍵查詢, 就可以用DbSet的Find方法:

這個方法有個優點, 就是如果這條資料已經在Context裡面追蹤了, 那麼查詢的時候就不查資料庫了, 直接會返回記憶體中的資料.

EF.Functions.Like 這個方法是新方法, 就像是Sql語句裡面的Like一樣, 或者字串的Contains方法:

這個感覺更像Sql語句, 輸出到Console的Sql語句如下:

這裡還要談的是First/FirstOrDefault/Last/LastOrDefaut方法.

使用這些方法必須先使用OrderBy/OrderByDescending排序. 雖然不使用的話也不會報錯, 但是, 整個過程就會變成這樣, context把整個表的資料家在到記憶體裡, 然後返回第一條/最後一條資料. 如果表的資料比較多的話, 那麼就會有效能問題了.

更新資料.

很簡單, context所追蹤的model屬性變化後, SaveChanges就會更新到資料庫.

當然, 多個更新操作和插入等操作可以批量執行.

離線更新.

就是這種情況, 新的context一開始並沒有追蹤one這個資料. 通過使用Update方法, 追蹤並設定狀態為update. 然後更新到資料庫.

可以看到, 在這種情況下, EfCore會更新該model到所有屬性.

Update同樣也有DbSet的UpdateRange方法, 也有context到Update和UpdateRange方法, 這點和Add是一樣的.

還有一種方法用於更新, 這個以後再說.

刪除資料.

DbContext只能刪除它追蹤的model.

非常簡單, 從log可以看到, 刪除動作只用到了主鍵:

如果是刪除的離線model, 那麼Remove方法首先會讓Dbcontext追蹤這個model, 然後設定狀態為Deleted.

刪除同樣有RemoveRange方法.

Raw SQL查詢/命令:

這部分請看文件:

命令: DbContext.Database.ExecuteSqlCommand();

查詢: DbSet.FromSql() https://docs.microsoft.com/en-us/ef/core/querying/raw-sql;

這個方法目前還有一些限制, 它只能返回實體的型別, 並且得返回domain model所有的屬性, 而且屬性的名字必須也得一一對應. SQL語句不可以包含關聯的導航屬性, 但是可以配合Include使用以達到該效果(https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#including-related-data).

更多的傳遞引數方式還需要看文件.

查詢和儲存關聯資料.

插入關聯資料.

我之前忘記在Department裡面新增Name欄位了, 現在新增一下, 具體過程就不寫了.

插入關聯資料有幾種情況:

1.直接把要新增的Model的導航屬性附上值就可以了, 這裡的Department不需要寫外來鍵.

看一下Sql:

這個過程一共分兩步: 1 插入主表, 2,使用剛插入主表資料的Id, 插入子表資料.

2.為資料庫中的資料新增導航屬性.

這時, 因為該資料是被context追蹤的, 所以只需在它的導航屬性新增新記錄, 然後儲存即可.

3.離線資料新增導航屬性.

這時候就必須使用外來鍵了.

預載入關聯資料 Eager Loading.

也就是查詢的時候一次性把資料和其導航屬性的資料一同查詢出來.

看看SQL:

這個過程是分兩步實現的, 首先查詢了主表, 然後再查詢的子表. 這樣做的好處就是效能提升.

(FromSql也可以Include).

預載入子表的子表:

可以使用ThenInclude方法, 這個可以老版本ef沒有的.

這裡查詢Department的時候, 將其關聯表Company也查詢了出來, 同時也把Company的關聯表Owner也查詢了出來.

查詢中對映關聯資料.

使用Select可以返回匿名類, 裡面可以自定義屬性.

這個匿名類只在方法內有效.

看下SQL:

可以看到SQL中只Select了匿名類裡面需要的欄位.

如果需要在方法外使用該結果, 那麼可以使用dynamic, 或者建立一個對應的struct或者class.

使用關聯導航屬性過濾, 但是不載入它們.

SQL:

這個比較簡單. 看sql一切就明白了.

修改關聯資料.

也會分兩種情況, 被追蹤和離線資料.

被追蹤的情況下比較簡單, 直接修改關聯資料的屬性即可:

看一下SQL:

確實改了.

這種情況下, 刪除關聯資料庫也很簡單:

看下SQL:

刪除了.

下面來看看離線狀態下的操作.

這裡需要使用update, 把該資料新增到context的追蹤範圍內.

看一下SQL:

這個就比較怪異了.

它update了該departmt和它的company以及company下的其他department和company的owner. 這些值倒是原來的值.

這是因為, 看上面的程式碼, 查詢的時候department的關聯屬性company以及company下的departments和owner一同被載入了.

儘管我只update了一個department, 但是efcore把其他關聯的資料都識別出來了.

從DbContext的ChangeTracker屬性下的StateManger可以看到有多少個變化.

這一點非常的重要.

如何避免這個陷阱呢?

可以這樣做: 直接設定dbContext.Entry().State的值

這時, 再看看SQL:

嗯. 沒錯, 只更新了需要更新的物件.

 

 

2.1版本將於2018年上半年釋出, 請檢視官網的路線圖: https://github.com/aspnet/EntityFrameworkCore/wiki/roadmap

完. 

下面是我的關於ASP.NET Core Web API相關技術的公眾號--草根專欄:

相關文章