Mohsen Mirshahreza

一個以資料庫為主、基於明確 DDL 的 .NET ORM — 執行階段發出的型別、免 JOIN 的 eager loading、基於 CDC 的推送通知,以及直接從 schema 產生 GraphQL/OpenAPI。

如果您曾在 Node.js 世界中使用過 Sequelize.js,您會熟悉這種感覺:指向一個資料庫、定義(或從既有資料庫反向工程)一個模型,然後在幾分鐘內就能用 Model.findAll({ where: {...}, include: [...] }) 開始查詢。Association、lifecycle hook、read replica — 全部都有,而且一切都夠貼近 SQL,讓您從不覺得自己正在與一個抽象層搏鬥。

.NET 的 ORM 故事截然不同。Entity Framework Core 是一套出色的函式庫 — 但本質上是 code-first:您定義 DbContext/entity 類別,再由 migration 說明資料庫應如何變更以與您的程式碼保持同步。當您的應用程式擁有 schema 的所有權時,這種模式非常棒。但當資料庫已經存在、屬於另一個團隊(甚至另一個程式碼庫,可能使用完全不同的語言),或您根本不希望應用程式程式碼決定 schema 的樣貌時,這種模式就行不通了。

正是這個缺口促成了 SequelizeDotNet 的誕生。

這套函式庫實際上做了什麼
SequelizeDotNet 是一套 database-first 理念的 .NET ORM,支援六種關聯式資料庫 — SQL Server、SQLite、PostgreSQL、MySQL/MariaDB、Oracle 以及 DB2 — 並建立在三項不可談判的設計決策之上:

資料庫永遠是唯一真相來源。它永遠不會根據您自己撰寫的 schema 來推測您的程式碼「應該」長什麼樣子。模型來自資料庫,而非反之亦然。
DDL 總是明確的,永不自動 diff。結構變更只能透過命名清楚的方法執行 — RenameColumnAsync、AddColumnAsync、DropTableAsync — 這些方法都是由開發者刻意呼叫。破壞性操作需要 force: true 旗標,且在 production 環境中預設鎖定,除非明確覆寫。沒有任何類似 Sequelize 的 sync({ alter: true })(「讓資料庫以任何必要方式與模型同步」)的功能 — 因為正是這類功能讓自動同步在 production 環境中變得危險,連 Sequelize 自己的文件都因此而建議不要使用。
執行階段型別,而非程式碼產生。在這裡 .NET 能做到 JavaScript 做不到的事:EntityTypeFactory 使用 System.Reflection.Emit 為每張資料表建立真正的 CLR 型別,並以 per-table 方式進行延遲且執行緒安全的快取(只會 emit 一次,即使在並行負載下)。只要在資料庫中變更一欄,下次對該資料表的查詢就會傳回一個反映此變更的新發出型別 — 無需重新編譯、無需重新啟動,也無需維護同步的 .cs 產生檔。
Sequelize 本身沒有的多項功能
因為 .NET 的執行階段與型別系統允許建立這些功能:

巢狀與多對多 Include,永不使用 JOIN。Include("Author.Publisher.Country") 會以三個獨立、已快取且以 IN (...) 篩選的查詢來遍歷三層 foreign key。IncludeMany("Tags", "dbo", "PostTags", "FK_PostTags_Posts", "FK_PostTags_Tags") 則透過明確命名的中介資料表完成相同操作。永遠不會有任何東西隱藏在模糊的自動查詢計畫後面。
真正理解原始 SQL 的讀寫路由。Sequelize 的 read replication 支援曾有重複的 bug 報告,讓原始查詢靜默地被導向 replica pool。StatementIntentClassifier 會檢查原始查詢的開頭關鍵字,確保即使是最簡單的捷徑也能正確路由。
Schema 的無需重新啟動熱重載。SchemaWatcher 會輪詢、比對,並以原子且具版本的方式替換已變更資料表的執行階段型別 — 無需重新啟動處理程序。
SQL Server 的 Change Data Capture 轉為可訂閱事件。大多數 ORM 都沒有內建的 CDC 支援。CdcChangeWatcher 會將 SQL Server 的變更資料表轉換為 C# 事件,您只需訂閱一次即可。
直接從 introspected schema 產生 GraphQL 與 OpenAPI。無需任何額外的模型步驟 — 只需讀取一次 schema,即可直接從資料庫已知的內容建立輕量 GraphQL schema 或 OpenAPI 3.0 文件。
目前確切狀態 — 誠實說明
這是一個年輕的專案(0.1.0 版),我希望精確說明到目前為止真正測試過的程度,而非僅列出已實作的功能:

SQL Server 與 SQLite 已完整並即時測試 — 超過 140 項測試涵蓋這兩個資料庫,包括所有資料型別、所有查詢運算子、交易、hook、schema drift、hot-reload 以及 bulk insert,並在真實的 LocalDB 與臨時資料庫上執行。
PostgreSQL、MySQL、Oracle 與 DB2 已完整實作於相同的 IDialect 合約之下,但僅在 unit-test(查詢轉譯器)層級進行測試 — 我尚未在這些資料庫上驗證端對端行為。如果您有,請務必貢獻 — 將現有的 SQL Server/SQLite 即時測試移植到其中任一資料庫,是目前最有價值的貢獻。
總計 223 項自動化測試,零失敗,於此版本中。
如果您的程式碼庫已擁有自己的 schema,並且想要編譯時期檢查的 LINQ,那麼 EF Core 可能仍是較佳的預設選擇 — 它更成熟、由 Microsoft 支援,且擁有更大的生態系。SequelizeDotNet 針對的正是資料庫是唯一真相來源、且您偏好以 Sequelize 過去十年教導一代開發人員的方式來工作的特定情境。

立即試用
dotnet add package SequelizeDotNet.Core
dotnet add package SequelizeDotNet.Providers.SqlServer
var engine = new QueryEngine(new SqlServerDialect(), new ModelRegistry());

var query = QueryEngine.Query("dbo", "Posts")
.Where("AuthorId", QueryOperator.Equal, 42)
.Include("FK_Posts_Authors");

var rows = await engine.ToListAsync(query, connection);
完整文件:Developer Guide · 原始碼與 issue:github.com/mirshahreza/SequelizeDotNet · 授權:MIT。

衷心感謝所有至今貢獻 Sequelize.js 的人 — 本專案之所以存在,是因為該專案十多年來證明了這種與資料庫互動的方式,在任何語言中都值得擁有。

如果您曾因遷移工具的錯誤推測而受傷,或您正在維護一個資料庫屬於另一個團隊的 .NET 服務,我很樂意聽取您的回饋 — 如果您有 PostgreSQL、MySQL、Oracle 或 DB2 的實例可以讓我們在其上執行測試套件,我會更加高興。

SequelizeDotNet · MIT 授權 · github.com/mirshahreza/SequelizeDotNet