Entity Framework Core (EF Core) 是一個提供了實體、關係映射的架構,通過它們,可以創建映射到資料庫的類型,使用 LINQ 創建資料庫查詢、新增和更新物件,把它們寫入資料庫。
Entity Framework 經過多年的改變,Entity Framework Core (EF Core) 已完全重寫了。不僅可以在 Windows 上使用,也可以在 Linux 和 Mac 上使用。他支持關聯式資料庫和 NoSQL 數據存儲。
這裡要使用 EF Core 建立一個簡單的模型讀寫來自 Oracle 資料庫 DEPT 與 EMP 資料表的資料,所以這裡會有 Dept 與 Emp 兩個類型映射到 Oracle 資料庫的這兩個資料表。把資料寫入資料庫,讀取、更新和刪除它們。
我使用 Visual Studio Code 開發,所以你的機器必須安裝 .NET Core SDK 。你如果要使用 EF Core 的反向工程 ,則還必須安裝Entity Framework Core 工具參考 - .NET CLI 。
反向工程可以幫你把資料庫的資料表(Table)產生 C# 的類型與映射,開始先不談反向工程,以免失焦及複雜化,後面再來補充說明。
開啟 VS Code 建立一個專案。
$ > dotnet --version 3.1.201 $ > dotnet new console --name OracleEFCoreSample The template "Console Application" was created successfully. Processing post-creation actions... Running 'dotnet restore' on OracleEFCoreSample\OracleEFCoreSample.csproj... I:\u01\projects\OracleEFCoreSample\OracleEFCoreSample.csproj 的還原於 238.4 ms 完成。 Restore succeeded. $ > cd OracleEFCoreSample $ OracleEFCoreSample> dotnet run Hello World!
這就開始了,直接使用 VS Code 打開 OracleEFCoreSample 專案目錄,首先要安裝專案需要的 NuGet Packages,可以到 NuGet Gallery 尋找,注意不要裝錯版本。
$ OracleEFCoreSample> dotnet add package Oracle.EntityFrameworkCore --version 2.19.70 $ OracleEFCoreSample> dotnet add package Microsoft.EntityFrameworkCore.Design --version 2.2.6 $ OracleEFCoreSample> dotnet add package Microsoft.EntityFrameworkCore.Relational --version 2.2.6 $ OracleEFCoreSample> dotnet add package Microsoft.Extensions.Configuration.Json --version 3.1.4
現在專案目錄下的 OracleEFCoreSample.csproj 檔案應該如下:
OracleEFCoreSample.csproj 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <Project Sdk ="Microsoft.NET.Sdk" > <PropertyGroup > <OutputType > Exe</OutputType > <TargetFramework > netcoreapp3.1</TargetFramework > </PropertyGroup > <ItemGroup > <PackageReference Include ="Microsoft.EntityFrameworkCore.Design" Version ="2.2.6" /> <PackageReference Include ="Microsoft.EntityFrameworkCore.Relational" Version ="2.2.6" /> <PackageReference Include ="Microsoft.Extensions.Configuration.Json" Version ="3.1.4" /> <PackageReference Include ="Oracle.EntityFrameworkCore" Version ="2.19.70" /> </ItemGroup > </Project >
一切就緒,首先在專案目錄下開一個 JSON 檔案 appsettings.json, 將專案程式的設定放在此,目前要放的是 Oracle 資料庫的 ConnectionString。
appsettings.json 1 2 3 4 5 6 7 { "Data" : { "DefaultConnection" : { "ConnectionString" : "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;" } } }
創建模型 這裡要定義兩個實體類型 Dept 與 Emp,分別映射到 Oracle 資料庫 DEPT 與 EMP 資料表。
在專案目錄下建立一個子目錄 Models 來擺放 Dept.cs 與 Emp.cs。
Models/Dept.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System;using System.Collections.Generic;namespace OracleEFCoreSample.Models { public partial class Dept { public Dept ( ) { Emp = new HashSet<Emp>(); } public int Deptno { get ; set ; } public string Dname { get ; set ; } public string Loc { get ; set ; } public virtual ICollection<Emp> Emp { get ; set ; } } }
在 Oracle 資料庫中,DEPT 與 EMP 有 FOREIGN KEY Constraint 的關係,在這個類型中也可以表現出這種 Master-Detail 關係,如果無此需求,可以省略。其實我們只需要第 13、14 與 15 行三個 Deptno、Dname 與 Loc 屬性就可以。
這裡這個 Dept 類型除了三個基本屬性外,Emp 可以把屬於這個部門的員工放進來。
Models/Emp.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using System;using System.Collections.Generic;namespace OracleEFCoreSample.Models { public partial class Emp { public Emp ( ) { InverseMgrNavigation = new HashSet<Emp>(); } public int Empno { get ; set ; } public string Ename { get ; set ; } public string Job { get ; set ; } public int ? Mgr { get ; set ; } public DateTime? Hiredate { get ; set ; } public double ? Sal { get ; set ; } public double ? Comm { get ; set ; } public int ? Deptno { get ; set ; } public virtual Dept DeptnoNavigation { get ; set ; } public virtual Emp MgrNavigation { get ; set ; } public virtual ICollection<Emp> InverseMgrNavigation { get ; set ; } } }
EMP 資料表有兩個 FOREIGN KEY Constraint,一個是 Deptno 參照到 DEPT 的 Deptno,另外一個是 Mgr 參照到自己的 Empno。一樣你如果不需要這些關聯,可以只要 13 ~ 20 行的 8 個屬性就可以。型別後面的 ? 號,表示這是個 Nullable value types。
除了 8 個基本屬性外, DeptnoNavigation 可以把部門資料放進來,MgrNavigation 可以把主管資料放進來,而如果是主管,可以把部屬資料放入 InverseMgrNavigation。
這兩個型別只是單純的 C# 類別型態定義,與 Oracle 資料庫資料表還看不出映射關係,EF Code 使用了三個概念來定義模型 : 約定、注釋和流利 API(Fluent API)。
按照約定,有些事情會自動發生。例如,用 Id 前綴命名 int 或 Guid 類型的屬性,會將該屬性映射到資料庫的主鍵。例如 EmpId 屬性會自動映射到資料表的主鍵 EmpId。但我們的 Oracle 資料表主鍵通常不會遵守這種規則,所以這裡要用另外一種映射到資料表的約定 : 使用上下文的屬性名 (Fluent API)。
創建上下文 (Context) 這裡要創建另一個 DemoContext 類,透過這個類實現了與 Oracle 資料庫表 DEPT 與 EMP 的關係。這個類派生自基類 DbContext。DemoContext 類定義了 DbSet 與 DbSet 的 Dept 與 Emp 屬性。
透過 DbContext 派生類的 OnModelCreating 方法使用流利 API。Entity 方法返回一個 EntityTypeBuilder,允許自定義實體,把它映射到特定的資料庫表名(Table)、定義鍵(Key)和索引(Indexes)。
EntityTypeBuilder 定義了一個 Property 方法來配置屬性。Property 方法返回一個 PropertyBuilder,它允許用最大長度、需要的設置和 SQL 類型配置屬性。
要定義一對多映射,EntityTypeBuilder 定義了映射方法。方法 HasMany 與 WithOne 結合,HasMany 需要與 WithOne 鏈接起來。
方法 HasOne 需要和 WithMany 或 WithOne 鏈接起來。鏈接 HasOne 與 WithMany,會定義一對多關聯;鏈接 HasOne 與 WithOne,定義一對一關係。
Models/DemoContext.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 using System;using System.IO;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.Configuration.Json;namespace OracleEFCoreSample.Models { public partial class DemoContext : DbContext { public virtual DbSet<Dept> Dept { get ; set ; } public virtual DbSet<Emp> Emp { get ; set ; } protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder ) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseOracle(GetConnectionString(), options => options.UseOracleSQLCompatibility("11" ) ); } } protected override void OnModelCreating (ModelBuilder modelBuilder ) { modelBuilder.HasAnnotation("ProductVersion" , "2.2.6-servicing-10079" ) .HasAnnotation("Relational:DefaultSchema" , "DEMO" ); modelBuilder.Entity<Dept>(entity => { entity.HasKey(e => e.Deptno) .HasName("SYS_C0036617" ); entity.ToTable("DEPT" ); entity.HasIndex(e => e.Deptno) .HasName("SYS_C0036617" ) .IsUnique(); entity.Property(e => e.Deptno).HasColumnName("DEPTNO" ); entity.Property(e => e.Dname) .HasColumnName("DNAME" ) .HasColumnType("VARCHAR2" ) .HasMaxLength(14 ); entity.Property(e => e.Loc) .HasColumnName("LOC" ) .HasColumnType("VARCHAR2" ) .HasMaxLength(13 ); }); modelBuilder.Entity<Emp>(entity => { entity.HasKey(e => e.Empno) .HasName("EMP_PK" ); entity.ToTable("EMP" ); entity.HasIndex(e => e.Empno) .HasName("EMP_PK" ) .IsUnique(); entity.HasIndex(e => e.Ename) .HasName("EMP_IDX1" ); entity.Property(e => e.Empno).HasColumnName("EMPNO" ); entity.Property(e => e.Comm) .HasColumnName("COMM" ) .HasColumnType("FLOAT" ); entity.Property(e => e.Deptno) .HasColumnName("DEPTNO" ); entity.Property(e => e.Ename) .HasColumnName("ENAME" ) .HasColumnType("VARCHAR2" ) .HasMaxLength(10 ); entity.Property(e => e.Hiredate) .HasColumnName("HIREDATE" ) .HasColumnType("DATE" ); entity.Property(e => e.Job) .HasColumnName("JOB" ) .HasColumnType("VARCHAR2" ) .HasMaxLength(9 ); entity.Property(e => e.Mgr).HasColumnName("MGR" ); entity.Property(e => e.Sal) .HasColumnName("SAL" ) .HasColumnType("FLOAT" ); entity.HasOne(d => d.DeptnoNavigation) .WithMany(p => p.Emp) .HasForeignKey(d => d.Deptno) .HasConstraintName("SYS_C0036621" ); entity.HasOne(d => d.MgrNavigation) .WithMany(p => p.InverseMgrNavigation) .HasForeignKey(d => d.Mgr) .HasConstraintName("SYS_C0036620" ); }); } private static string GetConnectionString ( ) { var configurationBuilder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json" ); IConfiguration config = configurationBuilder.Build(); string connectionString = config["Data:DefaultConnection:ConnectionString" ]; return connectionString; } } }
DemoContext 是資料庫映射的關鍵,聰明的你就對照著資料庫慢慢斟酌,GetConnectionString 方法則會從 appsettings.json 抓取 Oracle 資料庫的 ConnectionString。 這會用在第 19 行。
同時要注意第 20 行,因為我們的資料庫大部分還是 11g,所以要在 options 中加上 UseOracleSQLCompatibility(“11”),否則會有很多地方會掛掉。 建議趕快升級到 12c(以上)。
讀取資料庫 可以讀取資料庫了。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using System;using System.Linq;using OracleEFCoreSample.Models;namespace OracleEFCoreSample { class Program { static void Main (string [] args ) { Console.WriteLine("Hello Tainan! Hi 台南" ); using (var db = new DemoContext()) { Console.WriteLine("===== Departments ==========" ); foreach (var dept in db.Dept) { Console.WriteLine($"{dept.Deptno} {dept.Dname} {dept.Loc} " ); } Console.WriteLine("===== Employees ==========" ); foreach (var emp in db.Emp) { Console.WriteLine($"{emp.Empno} {emp.Ename} {emp.Job} {emp.Mgr} {emp.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )} {emp.Sal} {emp.Comm} {emp.Deptno} " ); } } } } }
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 ===== Departments ========== 10 會計部 紐約 20 RESEARCH DALLAS 30 SALES 台南 40 OPERATIONS BOSTON 70 資訊部 台南 B4 60 開發部 台南 ===== Employees ========== 7839 KING PRESIDENT 1981-11-17T00:00:00 5000 10 7698 BLAKE MANAGER1 7839 1981-05-01T00:00:00 2850 101 30 7782 陳瑞 MANAGER 7902 1981-06-09T00:00:00 2400 10 7566 陳賜珉 MANAGER 7839 1981-04-02T00:00:00 2975 20 7788 SCOTT ANALYST 7566 1982-12-09T00:00:00 45300 20 7902 FORD ANALYST 7566 1981-12-03T00:00:00 3000 20 7369 SMITH CLERK 7902 1980-12-17T00:00:00 8001 20 7499 ALLEN SALESMAN 7698 1981-02-20T00:00:00 1600 303 30 7608 馬小九 ANALYST 7788 2010-06-28T00:00:00 1000 100 40 7654 葉習? SALESMAN 7698 1981-09-28T00:00:00 1250 1400 30 7844 ??? SALESMAN 7698 1981-09-08T00:00:00 1500 30 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100 20 7900 JAMES CLERK 7698 1981-12-03T00:00:00 94998 30 7934 楊? CLERK 7902 1982-01-23T00:00:00 1500 10 9006 李逸君 ANALYST 7788 2001-05-07T00:00:00 66666 70 7607 ??? 分析師 7788 2008-03-24T00:00:00 45000 100 70 7609 蔡大一 分析師 7788 2010-06-28T00:00:00 60000 70 9011 文英蔡 總鋪師 7788 2018-08-28T00:00:00 77778 180 40 8907 牸? ANALYST 7566 1982-12-09T00:00:00 9002 10
模型建立好,讀取資料就這麼簡單,加入 LINQ 看看。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 using (var db = new DemoContext()){ var employees = db.Emp .Where(e => e.Deptno == 10 || e.Deptno == 30 ) .OrderBy(e => e.Deptno) .ThenBy(e => e.Empno); foreach (var e in employees) { Console.WriteLine($"{e.Empno} {e.Ename} {e.Job} {e.Mgr} {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )} {e.Sal} {e.Comm} {e.Deptno} " ); } }
現在將 Master-Detail 資料關聯起來,DEPT 對 EMP 是一對多的關係。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using (var db = new DemoContext()){ var employees = db.Emp.ToList(); foreach (var dept in db.Dept) { dept.Emp = employees.Where(d => d.Deptno == dept.Deptno).ToList(); } var department = db.Dept.Where(d => d.Deptno == 20 ).FirstOrDefault(); Console.WriteLine($"=== {department.Deptno} {department.Dname} {department.Loc} ===" ); foreach (var e in department.Emp) { Console.WriteLine($"{e.Empno} {e.Ename} {e.Job} {e.Mgr} {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )} {e.Sal} {e.Comm} {e.Deptno} " ); } }
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 === 20 RESEARCH DALLAS === 7566 陳賜珉 MANAGER 7839 1981-04-02T00:00:00 2975 20 7788 SCOTT ANALYST 7566 1982-12-09T00:00:00 45300 20 7902 FORD ANALYST 7566 1981-12-03T00:00:00 3000 20 7369 SMITH CLERK 7902 1980-12-17T00:00:00 8001 20 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100 20
主管與部屬關係。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using (var db = new DemoContext()){ var employees = db.Emp.ToList(); var managers = employees.GroupBy(e => e.Mgr) .Select(grp => grp.FirstOrDefault()) .Select(e => new { Manager = e.Mgr ?? e.Empno }); Emp manager = null ; foreach (var m in managers) { manager = employees.Where(e => e.Empno == m.Manager).FirstOrDefault(); manager.InverseMgrNavigation = employees.Where(e => e.Mgr == manager.Empno).ToList(); } manager = employees.Where(e => e.Empno == 7788 ).FirstOrDefault(); Console.WriteLine($"=== {manager.Empno} {manager.Ename} ===" ); foreach (var e in manager.InverseMgrNavigation) { Console.WriteLine($"{e.Empno} {e.Ename} {e.Job} {e.Mgr} {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )} {e.Sal} {e.Comm} {e.Deptno} " ); } }
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 === 7788 SCOTT === 7608 馬小九 ANALYST 7788 2010-06-28T00:00:00 1000 100 40 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100 20 9006 李逸君 ANALYST 7788 2001-05-07T00:00:00 66666 70 7607 ??? 分析師 7788 2008-03-24T00:00:00 45000 100 70 7609 蔡大一 分析師 7788 2010-06-28T00:00:00 60000 70 9011 文英蔡 總鋪師 7788 2018-08-28T00:00:00 77778 180 40
新增紀錄 Add 方法把 Emp 物件添加到 DemoContext 上下文中,還沒有寫入資料庫中,SaveChanges 方法把 Emp 物件寫入資料庫。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using (var db = new DemoContext()){ db.Emp.Add(new Emp { Empno = 9588 , Ename = "C#範例" , Job = "ANALYST" , Mgr = 7566 , Hiredate = DateTime.Now, Sal = 23000 , Comm = 100 , Deptno = 40 } ); int records = db.SaveChanges(); Console.WriteLine($"{records} records saved to database." ); }
還可以用 AddRange 一次寫入多個物件。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using (var db = new DemoContext()){ db.Emp.AddRange( new Emp { Empno = 9681 , Ename = "C#範例2" , Job = "ANALYST" , Mgr = 7566 , Hiredate = DateTime.Now, Sal = 13000 , Comm = 130 , Deptno = 10 }, new Emp { Empno = 9682 , Ename = "C#範例3" , Job = "ANALYST" , Mgr = 7566 , Hiredate = DateTime.Now, Sal = 33000 , Comm = 330 , Deptno = 20 } ); int records = db.SaveChanges(); Console.WriteLine($"{records} records saved to database." ); }
更新紀錄 Program.cs 1 2 3 4 5 6 7 8 9 10 11 using (var db = new DemoContext()){ int records = 0 ; var employee = db.Emp.Where(e => e.Empno == 9588 ).FirstOrDefault(); if (employee != null ) { employee.Comm = 500 ; records = db.SaveChanges(); } Console.WriteLine($"{records} records updated." ); }
刪除紀錄 Program.cs 1 2 3 4 5 6 7 8 9 10 11 using (var db = new DemoContext()){ int records = 0 ; var employee = db.Emp.Where(e => e.Empno == 9588 ).FirstOrDefault(); if (employee != null ) { db.Emp.Remove(employee); records = db.SaveChanges(); } Console.WriteLine($"{records} records deleted from database." ); }
可以用 RemoveRange 刪除多筆資料。
Program.cs 1 2 3 4 5 6 7 using (var db = new DemoContext()){ var employees = db.Emp.Where(e => e.Empno == 9681 || e.Empno == 9682 ); db.Emp.RemoveRange(employees); int records = db.SaveChanges(); Console.WriteLine($"{records} records deleted from database." ); }
物件關係映射工具,如 EF Core,並不適用於所有場景。例如一次刪除很多資料或所有的資料就沒有那麼高效能,使用單個 SQL 語句可以刪除所有資料,會比每一個紀錄使用一個 DELETE 語句效能高得多。
反向工程 (Reverse Engineering) 在反向工程之前,您必須安裝 CLI 工具。
$ > dotnet tool install --global dotnet-ef
然後在你的專案目錄下執行反向工程,下面會產生 DEPT 與 EMP 的反向工程。 它會在專案目錄下產生子目錄 Models 與 Dept.cs、Emp.cs 與 ModelContext.cs。 先前例子的 Dept.cs、Emp.cs 與 DemoContext.cs 有經過修正,可以對照看看,尤其是 Emp.Sal 屬性的型別。
$ OracleEFCoreSample> dotnet ef dbcontext scaffold "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;" Oracle.EntityFrameworkCore --table EMP --table DEPT -o Models -f
下面則是 Schema 的所有資料表。
$ OracleEFCoreSample> dotnet ef dbcontext scaffold "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;" Oracle.EntityFrameworkCore -o Models -f