一个面向数据库、基于显式 DDL 的 .NET ORM —— 运行时生成的类型、无 JOIN 的 eager loading、基于 CDC 的 push 通知,以及直接从 schema 生成 GraphQL/OpenAPI。
如果你曾在 Node.js 世界中使用过 Sequelize.js,就会熟悉那种感觉:指向一个数据库,定义一个模型(或从现有数据库提取),不到几分钟就能用 Model.findAll({ where: {...}, include: [...] }) 进行查询。关联、生命周期钩子、只读副本——全部具备,一切都足够贴近 SQL,让你永远不会觉得在与抽象层作斗争。
.NET 中的 ORM 故事却截然不同。Entity Framework Core 是一款出色的库——但本质上是 code-first 的:你定义 DbContext/entity 类,迁移文件描述数据库应如何变更以匹配你的代码。当你的应用拥有 schema 的所有权时,这种模式非常出色。但当数据库已经存在、属于另一个团队(甚至另一个代码库,可能使用完全不同的语言),或者你根本不希望应用代码决定 schema 的形状时,这种模式就表现不佳。
正是这种空白催生了 SequelizeDotNet。
这个库究竟做了什么
SequelizeDotNet 是一个采用 database-first 理念的 .NET ORM,支持六种关系型数据库——SQL Server、SQLite、PostgreSQL、MySQL/MariaDB、Oracle 和 DB2——基于三项不可谈判的设计决策构建:
数据库始终是真理之源。绝不会从你自己编写的 schema 推测你的代码“应该”是什么样子。模型来自数据库,而非反之。
DDL 始终显式,绝非自动 diff。结构变更仅通过名称清晰的方法执行——RenameColumnAsync、AddColumnAsync、DropTableAsync——这些方法由开发者主动调用。破坏性操作需要 force: true,并在生产环境中锁定,除非被显式覆盖。不存在类似 Sequelize 的 sync({ alter: true })(“无论如何都要让数据库与我的模型同步”)——因为正是这类特性让自动同步在生产环境中变得危险,甚至 Sequelize 自己的文档也因此建议避免使用。
运行时类型,而非代码生成。.NET 在这里能做到 JavaScript 做不到的事:EntityTypeFactory 使用 System.Reflection.Emit 为每个表创建一个真正的 CLR 类型,这些类型按表缓存,保证惰性且线程安全(仅 emit 一次,即使在并发负载下)。如果你更改数据库中的一列,下一次针对该表的查询将返回一个反映此变更的新发出类型——无需重新构建、无需重启,也无需维护一个需要同步的 .cs 生成文件。
Sequelize 本身没有的几个特性
因为 .NET 的运行时和类型系统允许创建它们:
嵌套的 Include 和多对多,始终无 JOIN。Include("Author.Publisher.Country") 会通过三个单独的、已缓存的、用 IN (...) 过滤的查询处理三层外键。IncludeMany("Tags", "dbo", "PostTags", "FK_PostTags_Posts", "FK_PostTags_Tags") 通过一个明确命名的中间表完成同样工作。绝不会有任何东西隐藏在模糊的、自动生成的查询计划背后。
真正理解原始 SQL 的读/写路由。Sequelize 的只读复制支持有重复的 bug 报告,原始查询会静默地被发送到副本池。StatementIntentClassifier 检查原始查询的起始关键字,以确保即使是快捷路径也能正确路由。
无需重启的 schema 热重载。SchemaWatcher 轮询、比较,并以原子方式、版本化地替换已变更表的运行时类型——无需重启进程。
将 SQL Server 的变更数据捕获作为可订阅事件。大多数 ORM 没有内置的 CDC 支持。CdcChangeWatcher 将 SQL Server 的变更表转换为一个 C# 事件,你只需订阅一次即可。
直接从内省的 schema 生成 GraphQL 和 OpenAPI。无需单独的建模步骤——读取一次 schema,即可直接从数据库已知的内容构建一个轻量级的 GraphQL schema 或 OpenAPI 3.0 文档。
它目前所处的位置——实话实说
这是一个年轻的项目(0.1.0 版本),我想准确说明它真正经过测试的程度,而不仅仅是已实现的功能:
SQL Server 和 SQLite 已通过完整且真实的测试——超过 140 个测试覆盖了这两个数据库,包括每种数据类型、每个查询运算符、事务、钩子、schema drift、热重载和批量插入,均在真实的 LocalDB 和临时数据库上运行。
PostgreSQL、MySQL、Oracle 和 DB2 已完全实现,遵循相同的 IDialect 契约,但仅在 unit-test 级别(查询翻译器)进行了测试——我还没有这些数据库的真实样本可以验证端到端行为。如果你有,非常欢迎贡献——将现有的 SQL Server/SQLite 真实测试移植到其中任何一个数据库,是目前能提供的最有价值的帮助。
本版本共有 223 个自动化测试,零失败。
如果你的代码库已经拥有其 schema 的所有权,并且想要编译时检查的 LINQ,EF Core 可能仍然是更好的默认选择——它更成熟、由微软支持,并且拥有大得多的生态系统。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 · 源代码与问题:github.com/mirshahreza/SequelizeDotNet · 许可证:MIT。
衷心感谢所有迄今为止帮助过 Sequelize.js 的人——这个项目之所以存在,是因为那个项目十多年来证明了这种与数据库合作的方式,在任何语言中都值得拥有。
如果你曾因迁移工具的错误猜测而受挫,或者你正在维护一个 .NET 服务,其数据库属于另一个团队,我很乐意听到你的反馈——如果你有一个 PostgreSQL、MySQL、Oracle 或 DB2 的样本可以用来测试测试套件,我会更加高兴。
SequelizeDotNet · MIT 许可证 · github.com/mirshahreza/SequelizeDotNet
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.