我会在发现新的实践后,持续更新这篇文章。
对于 .NET 开发人员来说,EntityFramework 是一个超级易用的 ORM 库。虽然,尽管它使用方便,而且具有即插即用的功能,但老实说,要把它做好是相当困难的。
👍 将图层分离到不同的项目中
下面的解决方案结构是我经常觉得最有用的一种:
它可以让你干净利落地分离应用的责任。
👍 只有 DataLayer 项目应该对 EntityFramework 有依赖性--2020 年 4 月 21 日更新。
持久性实现的细节应该只有数据层知道。
数据层必须实现其他项目中定义的接口,这些类的具体实现应该在你选择的依赖容器中注册。
👍 将 DbContext 类保持在 DataLayer 项目的内部。
在 DataLayer 中创建一个客户端可以使用的 IServiceCollection 扩展方法。该方法应该向依赖注入框架注册 DbContext。
客户端不应该知道或关心持久性。
在客户端项目的 Startup 类中使用 IServiceCollection。
封装数据库上下文可以保证客户端不会直接访问进行数据库调用,另外,客户端项目不会对 EntityFrameworkCore 产生不必要的依赖。
👍 在 OnModelCreating()上使用类型配置。
在创建 DbContext 时,很容易直接在 OnModelCreating(Modelbuilder builder)方法里面配置域模型。但是这种方法会很快变得混乱。
相反,只使用 OnModelCreating 来扫描执行实体配置的类型。
然后创建单独的专用类,用于配置您添加到 DbSet<T>的不同类型。
在 Configure(builder)`里面,你会用 FluentApi 写出所有数据库特定的模型配置。
通过使用内置的.NET 密钥管理器,不惜一切代价避免检查你的 ConnectionString。
请注意,您可能需要添加这个 nuget 包:Microsoft.Extensions.Configuration.UserSecrets 到你的项目中。
用户的密钥存储在:
Windows %APPDATA%\Microsoft\UserSecrets<user_secrets_id>\secrets.json Mac ~/.microsoft/usersecrets//secrets.json
按照以下步骤添加用户密钥:
- 在你的 AppSettings 中,添加 ConnectionString 属性。它可能看起来像下面的片段--重要的是该值为空。
# appsettings.json { "ConnectionStrings": { "default": "" }, // Some other properties }
- 使用 terminl 将目录改为你的客户端项目。例如,你的 ASP.NET Web 应用程序的根目录。
cd To/Path/Of/Startup/Root
- 初始化用户的密钥,并设置 ConnectionString。
dotnet user-secrets init dotnet user-secrets set "ConnectionStrings:default" "connection_string"
👍 对于生产,将密钥存储在 KeyVault 或环境变量中。
最好的解决方案是将密钥存储在 KeyVault 中,例如 Azure KeyVault。但是,也可能只需将它们存储为环境变量,或存储在位于 Web 服务器本身的 AppSettings.json 文件中即可。
👍 为应用创建一个独立的数据库登录名和用户。
像其他数据库的普通用户一样查看应用程序。
万一有人误将 db 用户和密码检查到源码控制中,只需禁用应用程序的 db 用户并创建一个新的用户,比你的服务器管理员(sa)登录和 dbo 用户被泄露的代价要小。
🚨 应用程序的 DB 用户不能有不必要的权限。
你会给你的组织或团队中的任何人赋予测试或生产数据库的超级管理权限吗?
根据最小权限原则,向应用程序的数据库用户授予权限。应用程序应该只拥有执行其工作的最低限度的权限。
👍 从一个不同于应用程序正在使用的 DB 用户中运行迁移脚本
该建议与职责分离有关。应用程序不应负责创建数据库及其表。应用程序应该只是对这些对象进行操作。
领域模型
👍 持续性无视
专注于对你的领域进行建模。尽量不要去想你使用的是哪个 ORM。
但是,要实事求是。
有时候,当表现得像持久性不存在时,让事情运转起来简直太麻烦了。考虑一下你的用例,以及你希望你的代码有多灵活和模块化。
🚨 避免数据注释。
避免使用[Table], [Column], [Key], [ForeignKey]等属性。
使用单独的数据层项目,对 Domain 项目进行依赖,并使用 IEntityTypeConfiguration<T>对所有模型进行链接。
👍 保持 ID 的私密性
除了方便之外,你很少会想暴露 ID。
在像 api/author/1 这样的 urls 中暴露 ID 可能会导致问题,比如如果 MS SQL 服务器已经崩溃或重启,下一个 ID 可能是 1001。我有客户评论过这个问题,坚持认为这是一个必须解决的 bug。
如果你试图用一个有私有 id 字段的类来生成迁移,EF core 会报错,因为它无法找到一个 ID 来作为主键。使用类型配置可以轻松解决这个问题,如下图所示:
与其使用 ID 查询,不如使用另一个唯一的属性,或者,属性的组合。
在不确定的情况下,Medium 文章似乎使用了作者句柄、文章名和随机字符串的混合。
nmillard/entityframework-core-dont-get-burnt-in-production-335ddfcfdfda
如果你必须公开暴露 ID,可以尝试使用其他东西而不是整数。
👍 自己生成 ID
不要让数据库等第三方软件来生成你的域名 ID。你应该完全控制它们的生成。它们实在是太重要了。
我通常采用两种方式之一来处理这个问题。1)让领域模型自己生成 ID,比如在实例化时分配一个 GUID,或者 2)使用一个 ID 工厂,并将生成的 ID 传递给模型的构造函数。
🚨 避免域模型中的外键属性。
👍 当 EF 不能生成外键列时,使用影子属性--2020 年 4 月 1 日添加。
🚨 避免使用公共缺省构造函数。
不要为了让 EF Core 高兴而做公共的缺省构造函数,而是要把缺省构造函数做成私有的。
如果连一个私有的构造函数都不够好,也可以有参数化的构造函数。不过,你应该注意这种行为。
- 参数和属性类型和名称必须匹配--但参数名称可以用驼峰大写(如 This)。
- 不能设置导航属性
注意上面的模型没有默认的公共构造函数。EF Core 用自动属性匹配构造函数参数。
👍 删除不必要的属性和字段的设置符。
使用数据层项目让 EF Core 知道如何在没有设置器的情况下填充属性和字段。
这是用实现 IEntityTypeConfiguration<T>的类来完成的。
我使用的是一个拥有类型的例子,该类型将被用作值对象--这就是为什么你会看到所有额外的方法和操作符重载来检查平等。
我使用的是一个值对象的例子,因为这时你通常会想要摆脱所有的 setter--甚至是内部或私有的 setter。
👍 将集合改为只读
为集合使用私有字段,并使用不可变的集合来暴露它们,如 IReadOnlyCollection<T>或 IReadOnlyList<T>。然后让 EF 核心知道如何在检索时填充私有集合,通过配置它的元数据导航属性,如下所示:
通过调用 Author 的方法只允许添加和删除书籍,你把逻辑集中起来了。
👍 使用自有类型
自有类型是一种只会出现在其他类型的导航属性上的类型。
自有类型通常不是实体本身,而是依赖于其他类型存在或价值对象的类型。这些类型最好不要有 id。
尽管 RecommendationScore 在 POCO 中没有主键,但数据库中会为它生成一个主键。它采用了作者的主键,因为我们已经配置好了,作者只能有一个 RecommendationScore。
dotnet ef CLI
🚨 不要使用 Visual Studio 帮助命令
在我看来,Visual Studio 隐藏了太多的东西,当你在使用.NET 时,包括 EF Core。
习惯了魔法,就会阻碍你排除故障的能力,并使你局限于 VS。
👍 使用 dotnet ef 命令行接口。
安装 dotnet ef 来执行 EF 核心命令。
dotnet tool install --gobal dotnet-ef
在你的数据层项目中,安装 Microsoft.EntityFrameworkCore.Design。这个包被 EF CLI 用来执行迁移、更新数据库等。
dotnet add package Microsoft.EntityFrameworkCore.Design
当使用 CLI 时,你将获得一些宝贵的、可转移的技能。你会更容易地设置 Code as Infrastructure、DevOps 管道、调试等。
记住,只有 Visual Studio 才知道自己的魔力。当你开始构建管道时,你可以不再依赖 Visual Studio 命令。像 Update-Database 这样的命令根本无法实现。
迁移
👍 建立简明、可管理的迁移。
每次对模型或模型配置进行更新时,请运行 ef migrations 命令。
dotnet ef migrations add MigrationName
尽量不要将不相关的模型更新捆绑到同一个迁移中,以保持迁移的可管理性。
👍 检查迁移代码是否正确
总是打开新创建的迁移,并阅读自动生成的代码。
很多时候我发现我需要调整我的模型配置,因为 EF Core 可能没有正确注册一个关系。
🚨 不要运行 dotnet ef database update
除了你自己的本地数据库之外,千万不要对其他任何东西运行数据库更新命令。
👍 使用幂等脚本创建和更新数据库
幂等脚本确保只有没有运行的语句才会被执行。当创建一个幂等数据库脚本时,你可以放心地多次执行整个脚本。
dotnet ef migrations script -v \ -o ./scripts/idempotent.sql \ --idempotent
上面的命令将使用所有现有的迁移生成一个脚本,并添加 IF 条件来检查是否已经执行了迁移。
将脚本保存在源代码控制中,在一个合理的位置。然后在你的部署过程中拿起并执行该脚本。
👍 为种子数据创建迁移
通过将你的种子数据放在迁移中,数据将自动成为幂等脚本的一部分。
这样做,你将会省去很多与之相关的头疼的事情,例如按顺序运行后续的脚本。
👍 记录迁移工作流程
很容易忘记重新创建或更新数据库所需的所有步骤。记录工作流程,并将文档标记文件保存在数据层项目根目录下。
便于复制到您自己的 .md 文件中。
## Intro All commands must be executed from the root of the data layer project. 1. cd to root of data layer project ### Add new migration dotnet ef migrations add <MigrationName> -s ../Path/To/StartupProj <MigrationName> ← name of the migration, without <> ### Remove most recent migration dotnet ef migrations remove -s ../Path/To/StartupProj ### Update local(!) database dotnet ef database update -s ../Path/To/StartupProj ^ this must only ever be used to update your own local database ### Generate idempotent script dotnet ef migrations script -v -i \ -o ./scripts/idempotent.sql \ -s ../Path/To/StartupProj ## Switches -v ← verbose console output -o ← path to where generated script file is placed -i ← makes the script idempotent -s ← Path to startup project (e.g. ASP.NET web app .csproj)