关系型数据库
本篇介绍关于关系型数据库设计与使用的最佳实践。
选择数据库
在.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实例。