.NET Core 1.0 開始為了跨平台重新改寫了 SQLite,名為 Microsoft.Data.Sqlite,刪除了過時的 DataTable 和 DataAdapter 相關的 DataSet API,這已經和之前的 System.Data.SQLite 不一樣,但團隊還是盡量讓它們兩者的 API 變化降到最低。這裡有官方的比較說明文件 與System.Data.SQLite的比較-Microsoft.Data.Sqlite | 微軟文檔
Microsoft.Data.Sqlite 使用 SQLitePCLRaw,它綑綁了 SQLite 所需要的底層物件,也會自動初始化環境,讓我們使用起來沒有太大的阻隔,不過我還沒有找到像
System.Data.SQLite 全域組件,目前就只能安裝在各個專案底下。
本來這篇是要寫 EF Core SQLite 的使用和限制,後來發現好像都沒有寫到 DbContext 的使用,就順手改了主題方向。
SQLite 限制
SQLite 畢竟沒有執行個體管理的檔案型資料庫,使用上的限制還是要了解一下,請參考官方文件
- ADO.NET limitations - Microsoft.Data.Sqlite | Microsoft Docs
- Async limitations - Microsoft.Data.Sqlite | Microsoft Docs
- Bulk insert - Microsoft.Data.Sqlite | Microsoft Docs
- SQLite Database Provider - Limitations - EF Core | Microsoft Docs
- Dapper limitations - Microsoft.Data.Sqlite | Microsoft Docs
- Xamarin limitations - Microsoft.Data.Sqlite | Microsoft Docs
SQLite 功能
- In-memory databases - Microsoft.Data.Sqlite | Microsoft Docs
- Encryption - Microsoft.Data.Sqlite | Microsoft Docs
- Online backup - Microsoft.Data.Sqlite | Microsoft Docs
- User-defined functions - Microsoft.Data.Sqlite | Microsoft Docs
- Custom SQLite versions - Microsoft.Data.Sqlite | Microsoft Docs
- Collation - Microsoft.Data.Sqlite | Microsoft Docs
- Blob I/O - Microsoft.Data.Sqlite | Microsoft Docs
- Interoperability - Microsoft.Data.Sqlite | Microsoft Docs
- Extensions - Microsoft.Data.Sqlite | Microsoft Docs
SQLite EF Core
骨子裡面也就是將 Microsoft.Data.Sqlite 再封裝一層,SQLite EF Core 有甚麼好處
- 整合 MS DI Container
- Mirgation 讓 SQLite 的檔案可以動態的長出來,就跟 SQL Server 一樣
記住,不是甚麼所有功能都有實現,它是有限制的,限制如下:SQLite Database Provider - Limitations - EF Core | Microsoft Docs
SQLite 管理工具
Rider Database
只要 Rider 的 Explorer 雙擊 sqlite 檔案,就可以直接連接它,第一次使用要安裝驅動,Rider 整合得很好不需要太費心,參考:Database connection | JetBrains Rider
DB Browser for SQLite
開發環境
- Rider 2021.1.1
- Windows 10
- Microsoft.EntityFrameworkCore.Sqlite 5.0.5
起手式
新增一個 .NET 5 Lib 專案,名為 Lab.DAL,並在 Rider 的 Terminal 視窗輸入以下命令
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 5.0.5
dotnet add package Microsoft.Extensions.Logging.Console --version 5.0.0
dotnet add package Microsoft.Extensions.Configuration.Json --version 5.0.0
EmployeeDbContext
- EmployeeDbContext 實作 DbContext
- DbSet<Employee> 代表的是 Employee 資料表
- this.Database.Migrate(),自動更新資料表
public class EmployeeDbContext : DbContext
{
private static readonly bool[] s_migrated = {false};
public virtual DbSet<Employee> Employees { get; set; }
public virtual DbSet<Identity> Identities { get; set; }
public virtual DbSet<OrderHistory> OrderHistories { get; set; }
public EmployeeDbContext(DbContextOptions<EmployeeDbContext> options)
: base(options)
{
if (s_migrated[0])
{
return;
}
lock (s_migrated)
{
if (s_migrated[0] == false)
{
var memoryOptions = options.FindExtension<InMemoryOptionsExtension>();
if (memoryOptions == null)
{
var sqliteOptionsExtension = options.FindExtension<SqliteOptionsExtension>();
if (sqliteOptionsExtension != null)
{
Console.WriteLine($"EmployeeDbContext 的連線字串為:{sqliteOptionsExtension.ConnectionString},執行 Migration");
}
this.Database.Migrate();
}
s_migrated[0] = true;
}
}
}
}
EF Core Code First 請參考 [EF Core 3] 如何使用 Code First 定義資料庫結構 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
完整代碼如下
sample.dotblog/EmployeeDbContext.cs at master · yaochangyu/sample.dotblog (github.com)
sample.dotblog/Employee.cs at master · yaochangyu/sample.dotblog (github.com)
sample.dotblog/Identity.cs at master · yaochangyu/sample.dotblog (github.com)
sample.dotblog/OrderHistory.cs at master · yaochangyu/sample.dotblog (github.com)
組態檔
appsettings.json 內容如下
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=bin\\Lab.DAL.db"
}
}
EmployeeContextFactory
這是用來讓 .NET Core CLI 套用的物件,連線字串來自於組態檔
public class EmployeeContextFactory : IDesignTimeDbContextFactory<EmployeeDbContext>
{
public EmployeeDbContext CreateDbContext(string[] args)
{
Console.WriteLine("EmployeeContextFactory - 由設計工具產生 Database,初始化 DbContextOptionsBuilder");
var config = DefaultDbContextManager.Configuration;
var connectionString = config.GetConnectionString("DefaultConnection");
Console.WriteLine($"EmployeeContextFactory - 讀取 appsettings.json 檔案的讀取連線字串為:{connectionString}");
var optionsBuilder = new DbContextOptionsBuilder<EmployeeDbContext>();
optionsBuilder.UseSqlite(connectionString);
Console.WriteLine($"EmployeeContextFactory - DbContextOptionsBuilder 設定完成");
return new EmployeeDbContext(optionsBuilder.Options);
}
}
EF Core Script
接下來用 Rider Terminal 或是 cmd 命令提示字元,先切換到 Lab.DAL 目錄
安裝全域通用工具
dotnet tool install --global dotnet-ef
Migration Add
dotnet ef migrations add InitialCreate
執行結果如下:
專案會多 Migrations 目錄,如下圖:
Update Database
產生 SQL 語法並執行
dotnet ef database update
或者用 dbContext.Database.Migrate
有關 Migration 請參考: [EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
DbContext 的設計
DbContext 以需求設計為出發點,一個需求對應一個 DbContext 和所需要的資料表欄位 (DbSet);而不是用一個 DbContext 裝載所有的 DbSet
下圖出自黃忠誠老師的 Entity Framework 效能全進化課程
DbContext 的生命週期
DbContext 實例的設計目的是要用於單一 UnitOfWork (工作單元), 這表示 DbContext 實例的存留期通常很短。
UnitOfWork 簡單來講,根據你的需求異動模型狀態,最後,再將最終的狀態,翻譯成命令一次丟給資料庫;而不是每一次模型異動就丟給資料庫
在上面的鏈接中引用 Martin Fowler 的話:「工作單元會跟踪您在業務交易過程中可能會影響數據庫的所有操作。完成後,它將計算出更改數據庫所需要做的一切,是你工作的結果。」
使用 Entity Framework Core(EF Core)時,典型的工作單元包括:
- 實例化 DbContext
- 通過上下文追蹤 Enitty 實例,Entity 追蹤包含
- 根據需求對 Entity 實例進行更改,以實現業務規則
- 調用 SaveChanges 或 SaveChangesAsync,EF Core 會檢測到 Entity 所做的更改,並將其寫入數據庫。
- 釋放 DbContext
重要的
- 使用後,釋放 DbContext 非常重要,這樣可以確保釋放所有非託管資源,並確保未註冊任何事件或其他掛鉤,以防止在實例仍被引用的情況下發生內存洩漏。
- DbContext不是線程安全的,不要在線程之間共享 Context(上下文),在繼續使用Context(上下文)實例之前,請確保等待所有異步調用。
- EF核心代碼引發的 InvalidOperationException 可使Context(上下文)處於不可恢復的狀態,此類異常表示程序錯誤,並非指從錯誤中恢復。
參考:DbContext Lifetime, Configuration, and Initialization - EF Core | Microsoft Docs
測試專案
當我需要 Survey / Study 的時候,我習慣使用測試專案來完成,新增一個 .NET 5 測試專案,名為 Lab.DAL.TestProject,並在 Rider 的 Terminal 視窗輸入以下命令
dotnet add package Microsoft.Extensions.Hosting --version 5.0.0
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 5.0.5
新增 appsettings.json,內容如下
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=Lab.DAL.TestProject.db"
}
}
手動管理 SQLite DbContext 執行個體
- 使用 ConfigurationBuilder 讀取 appsettings.json 組態的連線字串
參考:如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw) - 使用 LoggerFactory 紀錄 EF Core 轉譯的 SQL Raw 語法
參考:通過標準化的 Microsoft.Extensions.Logging 實現日誌紀錄 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw) - 實例化 DbContextOptionsBuilder 並設定 UseSqlite 以及 Logger,最後回傳 Options 屬性
private static DbContextOptions<EmployeeDbContext> CreateDbContextOptions()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var configRoot = configBuilder.Build();
var connectionString = configRoot.GetConnectionString("DefaultConnection");
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
//.AddFilter("Microsoft", LogLevel.Warning)
//.AddFilter("System", LogLevel.Warning)
.AddFilter("Lab.DAL", LogLevel.Debug)
.AddConsole()
;
});
return new DbContextOptionsBuilder<EmployeeDbContext>()
.UseSqlite(connectionString)
.UseLoggerFactory(loggerFactory)
.Options;
}
實例化 EmployeeDbContext 傳入 Options,
[TestMethod]
public void 操作真實資料庫_手動實例化EmployeeDbContext()
{
var contextOptions = CreateDbContextOptions();
using var dbContext = new EmployeeDbContext(contextOptions);
var id = Guid.NewGuid().ToString();
dbContext.Employees.Add(new Employee()
{
Age = 18,
Id = id,
CreateAt = DateTime.Now,
CreateBy = "test",
Name = "yao"
});
dbContext.SaveChanges();
var actual = dbContext.Employees.AsNoTracking().FirstOrDefault(p => p.Id ==id);
Assert.AreEqual(18,actual.Age);
Assert.AreEqual("yao",actual.Name);
}
通過 DI Container 管理 DbContext 執行個體
用通用性主機 .NET Generic Host 將 EmployeeDbContext 註冊到 DI Container,有以下方法
- IServiceCollection.AddDbContext
- IServiceCollection.AddDbContextPool
- IServiceCollection.AddDbContextFactory
- IServiceCollection.AddPooledDbContextFactory
接下來的範例我會
- 從 DI Container 取出 appsetting.json 組態檔的連線字串,組態請參考 如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- 設定 SQLite,builder.UseSqlite(connectionString)
通用性主機請參考:如何使用 .NET Generic Host for Microsoft.Extensions.Hosting | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
註冊到 DI Container 原始碼:efcore/EntityFrameworkServiceCollectionExtensions.cs at main · dotnet/efcore (github.com)
IServiceCollection.AddDbContext
用 services.AddDbContext<EmployeeDbContext> 設定
- DI Container 生命週期預設為 Scope,正好和 ASP.NET / ASP.NET Core 的 Request 的生命週期相同,常見的用法就是 Controller 依賴 DbContext,DbContext 宣告成 field,Request 結束 DbContext 也跟著結束
使用範例如下:
[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContext()
{
//arrange
var builder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddDbContext<EmployeeDbContext>((provider, builder) =>
{
var config = provider.GetService<IConfiguration>();
var connectionString = config.GetConnectionString("DefaultConnection");
builder.UseSqlite(connectionString);
});
});
var host = builder.Build();
var dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
using var dbContext = host.Services.GetService<EmployeeDbContext>();
//act
var id = Guid.NewGuid().ToString();
var now = DateTime.Now;
dbContext.Employees.Add(new Employee()
{
Id = id,
Name = "余小章",
Age = 18,
CreateAt = now,
CreateBy = "test user"
});
dbContext.Identities.Add(new Identity()
{
Employee_Id = id,
Account = "yao",
Password = "123456",
CreateAt = now,
CreateBy = "test user"
});
var count = dbContext.SaveChanges();
//assert
Assert.AreEqual(2, count);
using var db = new EmployeeDbContext(dbContextOptions);
var actual = db.Employees
.Include(p => p.Identity)
.AsNoTracking()
.FirstOrDefault();
Assert.AreEqual(actual.Name, "余小章");
Assert.AreEqual(actual.Age, 18);
Assert.AreEqual(actual.Identity.Account, "yao");
Assert.AreEqual(actual.Identity.Password, "123456");
}
IServiceCollection.AddDbContextPool
DbContext Pooling 是 EF Core 2 出來的功能,主要是用來提升高吞吐量情境的效能,在一般的情境不會有太大的感覺,
- 向 CI Container 請求 DbContext 實例時,會先檢查 Pool 有沒有可用的 DbContext 實例,有的話,則返回已存在的實例
- DbContext 實例使用完畢後,狀態會被清除;若 DbContext 有維護自已的狀態就不應該使用 Pool
參考 Advanced Performance 主題 | Microsoft Docs
- Pool 預設為 128
- 節省 DbContext 實例化的開銷
使用片段範例如下
[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextPool()
{
//arrange
var builder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddDbContextPool<EmployeeDbContext>(
(provider, builder) =>
{
var config =
provider.GetService<IConfiguration>();
var connectionString =
config.GetConnectionString("DefaultConnection");
var loggerFactory = provider.GetService<ILoggerFactory>();
builder.UseSqlite(connectionString)
.UseLoggerFactory(loggerFactory)
;
}, 64);
});
var host = builder.Build();
var dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
using var dbContext = host.Services.GetService<EmployeeDbContext>();
...
}
IServiceCollection.AddDbContextFactory
DbContextFactory 是 EF Core 5 出現的 What's New in EF Core 5.0 | Microsoft Docs,預設 DbContext從 DI Container 取出的生命週期為 Scope,和 Http Request 一樣,DbContextFactory 可以讓 DbContext 的生命週期更短
- IDbContextFactory 在 DI Container 的生命週期是 Singleton
- 用 DbContextFactory.CreateDbContext() 建立 DbContext 實例,用完即可釋放,可以讓 DbContext 的存活時間更短,我習慣讓他在一個方法內結束
efcore/IDbContextFactory.cs at main · dotnet/efcore (github.com)
使用範例如下:
[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextFactory()
{
//arrange
var builder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services
.AddDbContextFactory<EmployeeDbContext>((provider, builder) =>
{
var config = provider.GetService<IConfiguration>();
var connectionString =
config.GetConnectionString("DefaultConnection");
builder.UseSqlite(connectionString);
});
});
var host = builder.Build();
var dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
var dbContextFactory = host.Services.GetService<IDbContextFactory<EmployeeDbContext>>();
using var dbContext = dbContextFactory.CreateDbContext();
//act
var id = Guid.NewGuid().ToString();
var now = DateTime.Now;
dbContext.Employees.Add(new Employee()
{
Id = id,
Name = "余小章",
Age = 18,
CreateAt = now,
CreateBy = "test user"
});
dbContext.Identities.Add(new Identity()
{
Employee_Id = id,
Account = "yao",
Password = "123456",
CreateAt = now,
CreateBy = "test user"
});
var count = dbContext.SaveChanges();
//assert
Assert.AreEqual(2, count);
using var db = new EmployeeDbContext(dbContextOptions);
var actual = db.Employees
.Include(p => p.Identity)
.AsNoTracking()
.FirstOrDefault();
Assert.AreEqual(actual.Name, "余小章");
Assert.AreEqual(actual.Age, 18);
Assert.AreEqual(actual.Identity.Account, "yao");
Assert.AreEqual(actual.Identity.Password, "123456");
}
IServiceCollection.AddPooledDbContextFactory
IServiceCollection.AddDbContextFactory 和 IServiceCollection.AddDbContextPool 的綜合體
使用範例如下
[TestMethod]
public void 操作真實資料庫_由Host註冊EmployeeDbContextPoolFactory()
{
//arrange
var builder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddPooledDbContextFactory<EmployeeDbContext>(
(provider, builder) =>
{
var config =
provider.GetService<IConfiguration>();
var connectionString =
config.GetConnectionString("DefaultConnection");
var loggerFactory = provider.GetService<ILoggerFactory>();
builder.UseSqlite(connectionString)
.UseLoggerFactory(loggerFactory)
;
}, 64);
});
var host = builder.Build();
var dbContextOptions = host.Services.GetService<DbContextOptions<EmployeeDbContext>>();
var dbContextFactory = host.Services.GetService<IDbContextFactory<EmployeeDbContext>>();
using var dbContext = dbContextFactory.CreateDbContext();
//act
var id = Guid.NewGuid().ToString();
var now = DateTime.Now;
dbContext.Employees.Add(new Employee()
{
Id = id,
Name = "余小章",
Age = 18,
CreateAt = now,
CreateBy = "test user"
});
dbContext.Identities.Add(new Identity()
{
Employee_Id = id,
Account = "yao",
Password = "123456",
CreateAt = now,
CreateBy = "test user"
});
var count = dbContext.SaveChanges();
//assert
Assert.AreEqual(2, count);
using var db = new EmployeeDbContext(dbContextOptions);
var actual = db.Employees
.Include(p => p.Identity)
.AsNoTracking()
.FirstOrDefault();
Assert.AreEqual(actual.Name, "余小章");
Assert.AreEqual(actual.Age, 18);
Assert.AreEqual(actual.Identity.Account, "yao");
Assert.AreEqual(actual.Identity.Password, "123456");
}
相關文章
通用性主機請參考:
如何使用 .NET Generic Host for Microsoft.Extensions.Hosting | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
DI Container 請參考:
如何使用 DI Container for Microsoft.Extensions.DependencyInjection | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
如何使用 Microsoft.Extensions.DependencyInjection for Autofac | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
組態請參考:
如何使用組態 Microsoft.Extensions.Configuration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
如何使用 Options Pattern for Microsoft.Extensions.Options | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
EF Core 請參考
[EF Core 3] 安裝 EF Core 3 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[EF Core 3] 如何使用 Code First 定義資料庫結構 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[EF Core 3] 如何使用 Code First 的 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
EF 請參考
[EF6][SQLite] SQLite Code First 和 Migration | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[EF6][SQLite] SQLite Code First 和 Migration (2) | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
[SQLite] 解決佈署 Web 站台時出現 SQLite 問題 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
範例位置
sample.dotblog/ORM/EFCore/Lab.SQLite at master · yaochangyu/sample.dotblog (github.com)
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET