👨‍💻 niltor    📆 2026-08-27 17:44

关系型数据库

本篇介绍关于关系型数据库设计与使用的最佳实践。

选择数据库

在.NET生态中,第一选择是SQL Server,其次是PostgreSQL。

付费选择SQL Server,免费选择PostgreSQL。

简要原因

  • 自身功能强大,适用性广泛。
  • .NET生态有非常好的支持,背后都有微软开发团队的支持。
  • EF Core支持非常好,更新及时。

Note

新项目直接上SQL Server 2025+和PostgreSQL 18+。

操作数据库

使用Entity Framework Core

EF Core是微软官方的ORM框架,推荐使用EF Core来作为常规的数据访问方式,它也支持原生SQL查询,并经过官方和社区的优化,查询效率非常高。

批量操作

这里指专注于速度的大批量的数据插入、更新、删除等操作。

EF Core并不适合这类操作,你可以使用:EFCore.BulkExtensions,针对不同的数据库,使用不同的底层实现,来实现高效的批量操作。

数据库表结构设计

我们推荐一些常见的实践和指导,帮助你节省下在设计上争论的时间。

外键

❌ 统一不使用外键并不推荐,这会丧失关系型数据库的重要特性。

我们推荐你在同一领域模型中使用外键,以增强约束。而在跨领域模型中,有选择的使用外键。

典型场景就是用户实体,它通常是跨领域的,甚至是跨服务,跨数据库的,这种情况下,不应或无法使用外键关联。

字段类型选择

同样,为了减少争论花费的时间,我们推荐一些常见的字段类型选择。

  • ✅主键使用Guid,客户端生成,使用Guid V7。
  • ✅避免使用字符串分隔符表示多个值,也不使用关联表,直接使用数组类型(PostgreSQL)或者JSON类型(SQL Server)。
  • ✅时间类型使用DateTimeOffset,入库时,统一转换为UTC时间。
  • ✅日期类型使用DateOnly,入库时,统一转换为UTC日期。
  • ✅仅在必要时,使用乐观锁字段,比如RowVersion。

Tip

避免在无意义的争论中浪费时间,直接使用这些推荐的做法,除非它不符合你的业务需求。

数据库迁移

在开发迭代过程中,数据库结构也会更新,推荐使用Code First的方式,来集中管理数据库结构,避免由于手动操作导致的差异。

ApiStandard 的迁移架构

ApiStandard 在 AppHost 中使用 Aspire 官方的 AddEFMigrations,迁移资源挂在默认存在的AdminService上:

var adminMigrations = adminService
    .AddEFMigrations(
        "AdminService-Migrations",
        "EntityFramework.AppDbContext.DefaultDbContext"
    )
    .WithMigrationsProject("..\\Definition\\EntityFramework\\EntityFramework.csproj")
    .WithReference(database)
    .WaitFor(database)
    .RunDatabaseUpdateOnStart()
    .PublishAsMigrationBundle(publishContainer: true);
apiService.WaitForCompletion(adminMigrations);
adminService.WaitForCompletion(adminMigrations);
  • 本地 aspire start 时,RunDatabaseUpdateOnStart() 执行数据库更新,API 和后台服务等待迁移资源完成。
  • RunDatabaseUpdateOnStart() 只影响本地运行,不会代替生产发布阶段的迁移。
  • PublishAsMigrationBundle(publishContainer: true) 会生成迁移容器,供 Docker Compose、Kubernetes 等发布目标使用。
  • Kubernetes 发布时,模板通过 PublishAsKubernetesService 将迁移工作负载转换为 batch/v1 Job,并设置 restartPolicy: OnFailure。Job 成功后结束,不应作为长期运行的 API 服务。

迁移 bundle 是幂等的,但生产发布仍应先确认迁移 Job 成功,再向新版本 API 放流量。

参考 Aspire 官方文档:使用 AddEFMigrations 自动执行 EF Core migrations、按环境避免迁移容器重启。

生成迁移

在scripts目录下,提供了生成迁移的脚本EFMigrations.ps1,你可以直接使用它来生成迁移,或根据实际需要进行修改,生成的迁移文件默认会在EntityFramework/Migrations目录下。

脚本会读取 src/AppHost/appsettings.Development.json 中的 Components:Database 和 Components:IsMultiTenant,并转换为对应的环境变量,然后使用默认存在的 AdminService 作为启动项目、EntityFramework 作为迁移项目。Components:IsMultiTenant会传递给迁移进程,但不决定是否添加租户索引;租户索引由模型约定统一处理:

  • 实现ITenantEntityBase的实体会自动将TenantId放在索引前面;
  • 已经包含TenantId的索引不会重复添加;
  • Tenant是全局租户目录,模型会忽略它继承的TenantId。
.\scripts\EFMigrations.ps1 Init

添加实体或修改模型后,应先生成迁移,再通过 AppHost 验证本地更新流程。不要直接修改已经应用到生产环境的迁移文件。

初始化数据

默认数据库通过 EF Core UseSeeding 和 UseAsyncSeeding 创建default.com默认租户,并将默认业务数据库连接串写入DbConnectionString,将分析连接串写入AnalysisConnectionString;未配置独立分析连接时,分析连接使用默认业务连接。同步和异步路径使用相同的幂等判断,迁移重复执行不会重复插入默认租户。

默认租户是全局租户目录根节点,因此不会为它设置 TenantId;Tenant只受软删除过滤影响。普通业务实体始终按模板的租户隔离规则处理,即使配置中的IsMultiTenant为false也不会关闭TenantId字段、全局过滤器或保存校验。

参考:Seed data in a database using Aspire 和 EF Core Data Seeding。

多数据库支持

在EntityFramework项目的AppDbContext目录下,添加额外的DbContext。

然后在ServiceDefaults项目的FrameworkExtensions扩展类中,注入添加的DbContext。

在使用时,通过UniversalDbFactory工厂类,获取对应的DbContext实例。