0%

C# Entity Framework Core 2.0 and Oracle Provider

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