28
9

小提琴演奏主要基本功

0
归档:2024年9月分类:点滴生活

小提琴的演奏技术来说,要练好以下几个主要基本功:

一、运弓

优秀的演奏家能在小提琴上发出千变万化的声音,就运弓而言,取决于运弓的速度、弓在弦上的压力以及弓和弦的接触点这3种因素的不同结合。小提琴的弓法繁多,就其主要的有以下几种:
①分弓:一弓演奏一个音;
②连弓:一弓演奏许多音;
③顿弓:音与音之间断开;
④跳弓:弓毛离开琴弦。

这4类弓法是最基本的,在20世纪中期,连顿弓,即在一弓中连续快速演奏许多音与音之间是断开的音,被人视为绝技,所以人们把小提琴演奏艺术称之为“运弓的艺术”。

二、揉弦

通过左手手指在弦上的颤动,使声音的音高产生有规律的波动,称之为揉弦。

揉弦是小提琴演奏中极为重要的表现手段。揉弦分为手指的、手腕的和手臂的。优秀的演奏者不仅要掌握这3种揉弦方法,而且要学会使用不同速度和不同幅度的揉弦,在演奏不同作家,不同作品,不同乐句时,富于变化地运用揉弦。从揉弦的运用,可以使听众明显地辨认出不同演奏家的音乐个性。

三、把位

左手手指在指板上的位置,称之为把位。靠近琴头的把位为低把,靠近琴马的为高把。从一个把位换到另一个把位,称为换把。

换把位的方法有多种,例如空弦换把,同指换把,不同指以及泛音换把等。换把时产生非音乐需要的滑音,是技巧训练不足的标志。滑音可以使音与音之间的连接富于变化,特别是结合换把使用滑音,是一种富于表现力的演奏手段。

四、双音与和弦

小提琴可以同时演奏两个音甚至是3个音,也可以分奏4个音的和弦,这不仅丰富了它的表现力,并可不依赖其他乐器的伴奏进行单独演奏。小提琴的三度、六度、八度以及十度双音音阶,是演奏双音的基础,也是小提琴家必须终身练习的一项基本功。小提琴演奏中的左手颤音、泛音、拨弦等,都是一些高深的技巧。

09
9

随着前端技术的快速发展,React作为一款领先的JavaScript库,不断推动着前端开发的变革。近期,React官网的一个显著变化引起了广大开发者的关注:它不再推荐使用Create-React-App作为构建React应用的默认工具,而是转向了Next.js。

那么,Next.js究竟有何魔力,让React官网做出如此决策?本文将为你详细解析。

一、Next.js:React应用的“升级版”

Next.js并不是一个全新的框架,而是基于React的服务器端渲染框架。它提供了许多开箱即用的功能,使得开发者能够更高效地构建复杂、高性能的React应用。与Create-React-App相比,Next.js更像是一个“升级版”,它为React应用带来了以下显著的优势:

性能优化:Next.js支持自动静态优化(Automatic Static Optimization)和服务器端渲染(Server-Side Rendering),使得页面加载速度更快,用户体验更流畅。此外,Next.js还支持预渲染(Pre-rendering)和动态导入(Dynamic Imports),进一步提升了应用的性能。
数据获取简化:Next.js内置了数据获取功能,使得开发者能够更方便地从API或其他数据源获取数据,并将其注入到组件中。这种无缝的数据获取流程大大简化了开发过程,提高了开发效率。
强大的路由功能:Next.js提供了基于文件系统的路由功能,使得页面之间的导航变得简单直观。开发者只需创建相应的页面文件,Next.js就能自动为其生成路由。这种方式不仅易于理解,还能减少错误和提高可维护性。
更好的SEO支持:由于Next.js支持服务器端渲染和预渲染,因此它能够更好地处理页面渲染和加载问题,从而提高网站的搜索引擎优化(SEO)效果。

二、Create-React-App的局限性

虽然Create-React-App为React开发者提供了一个快速搭建项目的脚手架,但随着项目规模的扩大和复杂度的增加,其局限性也逐渐显现:

配置不灵活:Create-React-App为开发者提供了一套固定的配置,虽然这降低了入门门槛,但对于有特定需求的开发者来说,这种固定配置可能会成为束缚。
性能优化有限:Create-React-App主要关注于项目的快速搭建和开发体验,对于性能优化方面的支持相对有限。对于需要高性能的应用来说,开发者可能需要花费更多的时间和精力进行手动优化。
扩展性不足:随着项目的发展,开发者可能需要集成更多的功能和工具。然而,Create-React-App的扩展性相对有限,可能无法满足一些高级需求。

三、React官网推荐的背后

React官网之所以推荐Next.js而非Create-React-App,背后有多重原因。

首先,Next.js作为React的“升级版”,在性能、数据获取、路由等方面提供了更强大的支持,能够更好地满足现代Web应用的需求。
其次,随着前端技术的不断发展,开发者对于高性能、易扩展的应用框架的需求也在不断增加。Next.js正好符合这一趋势,能够为开发者提供更高效、更灵活的开发体验。
最后,React官网的推荐也反映了社区对于Next.js的广泛认可和支持。越来越多的企业和项目采用Next.js构建应用,证明了其在实际应用中的价值和优势。

四、结语

React官网推荐使用Next.js而非Create-React-App,是基于对现代Web应用需求的深入理解和对前端技术发展趋势的敏锐洞察。对于广大React开发者来说,了解和掌握Next.js将是一个值得投入时间和精力的选择。它将帮助你更高效地构建高性能、易扩展的React应用,提升你的开发能力和竞争力。

https://segmentfault.com/a/1190000044806695

25
7

Create React App 的演变

在 2016 年发布 Create React App 时,工具的环境是分散的。如果想要将 React 添加到现有应用,需要添加一个 script 标签或从 npm 中导入,然后调整现有的构建工具配置。但是,如果要从头开始创建一个仅使用 React 构建的新应用,则没有明确的方法可以做到这一点。
在 Create React App 之前,必须安装一堆工具并将它们连接在一起,提供正确的预设以使用 JSX,为开发和生产环境进行不同的配置,为资源缓存提供正确的设置,配置 linter 等,想要正确完成这一系列工作非常困难。人们通过创建和共享可以克隆的“样板”存储库来解决了这个问题。然而,这产生了另外一个问题:一旦在项目中调整了克隆的样板文件,就很难再拉取样板的更新。这样,项目的设置会变得旧,要么放弃更新,要么花费大量精力让所有工具再次协同工作。在快速发展的生态系统中,这非常困难。
Create React App 通过将多个工具组合在一个包中解决了这个问题。现在,如果想用 React 开始一个新项目,有一个明确的推荐方法(Create React App)可以做到这一点! 然后,每隔一段时间,可以更新这个包,以获得所有底层工具的更新。这种模型变得很流行,以至于今天有很多工具都以这种方式工作。Vite 确实是拥有相似愿景的最佳工具之一,并且在在某些方面更进一步。
Create React App 的目标是为大多数 React 用户提供启动新 React Web 应用的最佳方式,它支持一组协同工作的精选功能。随着时间的推移,它提供的开箱即用的“baseline”会随着我们找到正确的权衡而扩大。 例如,为运行时错误添加了一个遮罩层,添加了对不同样式选项的支持,默认添加了快速刷新,它允许保存组件的代码并查看更改而不会丢失状态。对于默认的 React 开发体验来说,这是一个巨大的里程碑。总的来说,由于 Create React App 完全控制了编译管道,因此添加编译相关的功能是很容易的。
有这样一个精心策划的设置对生态系统仍然很有价值。当 React Hooks 出现时,React 团队将 React Hooks lint 规则添加到默认设置中。除此之外,Create React App 还允许 React 团队向尽可能广泛的受众部署重要的工具更改(快速刷新支持、React Hooks lint 规则)。 如果没有 React 团队策划的流行模板,将很难如此广泛地推出这些工具更改。

Create React App 的问题

随着时间的推移,Create React App 停滞不前。许多人指出它比替代品慢,并且不支持人们如今想要使用的一些流行工具。原则上,React 团队可以解决这些问题。例如,可以更新Create React App 内部,以使用更快的 bundler,甚至在内部使用 Vite。或者可以建议人们从 Create React App 迁移到 Vite 这样的应用。然而,React 团队还想解决一个更深层次的问题。
按照设计,Create React App 会生成一个纯客户端应用。这意味着用它创建的每个应用都包含一个空的 HTML 文件、一个带有 React 的 script 标签和应用包。当加载空的 HTML 文件时,浏览器会等待 React 代码和全部应用包下载。这在低带宽连接上可能需要一段时间,并且此时用户在屏幕上看不到任何内容。然后,加载应用代码。此时用户会在屏幕上看到一些内容——但通常还需要加载数据。所以代码发送了加载数据的请求,用户需要等待它返回。 最后,数据加载,组件重新渲染数据,用户看到最终结果。
这是非常低效的,尽管如果只在客户端运行 React 很难做得更好。将其与 Rails 这样的服务端框架进行对比:服务端将立即启动数据获取,然后生成包含所有数据的页面。在这种情况下,用户会看到包含所有信息的 HTML 文件,而不是等待加载脚本的空白文件。HTML 是Web 的基石——那么为什么创建React 应用会产生一个空的 HTML 文件?为什么不利用 Web 最基本的功能——在所有交互代码加载之前快速查看内容的能力? 为什么要等到所有客户端代码加载完成后才开始加载数据?
Create React App 只解决了问题的一方面,它提供了良好的开发体验,但它没有强加足够的结构来帮助我们利用 Web 的强大功能获得良好的用户体验。开发者可以尝试自己解决这些问题,但这违背了 Create React App 的宗旨。每个真正高效的 React 设置都是自定义的、不同的,并且是 Create React App 无法实现的。
这些用户体验问题并不是 Create React App 特有的。它们甚至不特定于 React。例如,从 Preact、Vue、Lit 和 Svelte 的 Vite 主页模板创建的应用都会遇到相同的问题。这些问题是没有静态站点生成 (SSG) 或服务端渲染 (SSR) 的纯客户端应用所固有的。

React 框架的兴起

有些人可能不喜欢完全使用 React 进行构建。例如,可以在服务端或在构建过程中使用不同的工具(如 Jekyll 或 Astro)生成 HTML 页面。这解决了空 HTML 文件的问题,但是必须混合使用两种渲染现技术。随着时间的推移,想要添加的交互性越多,这种技术割裂就越明显。
这种割裂不仅会损害开发人员的体验——用户体验也会受到影响。使用真正以 HTML 为中心且未充分利用 React 的工具,每个页面导航都会变成完整的页面重新加载,从而清除了所有客户端状态。如今,许多用户希望在应用内导航顺畅,而不是 90 年代风格的整页重新加载。同样,许多开发人员更喜欢使用单一的渲染模型而不是混合两种不同的模型来构建他们的应用。开发者想用 React 构建整个应用。
如果使用 React 构建整个应用,那么能够使用 SSG/SSR 很重要。 在 Create React App 中缺乏对它们的支持。除此之外,经过多年的生态系统创新,React 的许多其他问题现在都有了成熟的解决方案。 例如,network waterfalls 和 bundle 大小。
即使应用没有像面向内容的网站那样从 SSG 或 SSR 中获益,它也可能会受到网络瀑布的影响。如果在挂载时获取数据,则在加载所有代码并渲染组件之前,第一次数据获取甚至不会开始。这是一个 waterfall:如果应用知道如何在代码仍在加载时开始获取数据,那么就可以并行完成。在导航中,如果父组件和子组件都需要获取某些内容,则会产生更糟糕的 waterfall。当我们谈论 React 性能时,无法回避一个事实:对于如此多的应用来说,waterfall 是性能的瓶颈。要解决这些 waterfall,需要将数据获取与路由集成起来,而Create React App 无法做到这一点。
我们的应用代码会随着添加的每个新功能和额外依赖项而不断增长。如果经常部署,应用在每次使用时加载速度可能会变得非常慢,因为它总是需要加载所有代码。有几种方法可以解决这个问题;可以移动一些代码以在服务端或在构建期间运行(如果工具允许)。理想情况下,还可以按路由拆分代码。然而,如果尝试手动进行代码拆分,通常会使性能更差。要解决这一问题,需要将数据获取与路由和打包相结合,而 Create React App 无法做到这一点。
React 本身只是一个库。它不规定如何使用路由或数据获取。 Create React App 也没有。不幸的是,这意味着单靠 React 和最初设计的 Create React App 都无法解决这些问题。服务端渲染和静态生成、数据获取、打包和路由都是相关联的。当 Create React App 发布时,React 还很新,如何让这些功能独立工作都还有很多东西需要弄清楚,更不用说如何完美地将它们组合在一起了。
时代在发展,现在,越来越难以推荐无法获得这些功能的解决方案。即使不立即使用它们,它们也应该在需要时可用,并且不必迁移到不同的模板并重新构建所有代码即可利用它们。 同样,并非所有数据获取或代码拆分都需要基于路由。但这是一个很好的默认设置,应该适用于大多数 React 应用。
虽然可以自己整合所有这些功能,但很难好。 就像 Create React App 本身集成了与编译相关的几个功能一样,Next.js、Gatsby 和 Remix 等工具跟进一步——将编译与渲染、路由和数据获取集成在一起。这类集编译、渲染、路由和数据获取于一体的工具被称为“框架”(或者,如果喜欢称 React 为框架的话,可以称它们为“元框架”)。这些框架提供了更好的用户体验。
React 作为一个架构
我们喜欢 React 的灵活性,可以使用 React 构建单个按钮,也可以使用它构建整个应用。 可以使用它在已有 20 年历史的 Perl 网站中构建仪表板,或者可以使用 React 制作混合 SSG/SSR 的电子商务网站。这种灵活性是必不可少的,用户也很喜欢它。
React 团队也希望为完全使用 React 构建的新应用提供更好的默认设置。如果默认建议的创建 React 应用的方法支持 SSG 和 SSR、自动代码拆分、路由预加载、保留客户端 UI 状态的导航以及其他可实现出色用户体验的功能,就太好了。至少,创建 React 应用的默认建议方式不应该完全被排除在这些功能之外,因为现有的仅客户端架构没有实现这些功能。
React 面临着挑战,帮助 React 框架提供出色用户体验的最佳方式就是专注于 React 的底层。 React 本身可以在渲染层做一些独特的事情,这些事情大大提高了框架在其他层的能力。例如,与一样,一个 React API 可以在幕后为框架解锁一系列框架优化。
React 是一个库,它提供了一些 API,可让定义和组合组件。 React 也是一种架构,它提供了让框架作者充分利用其渲染模型的构建块。我们可以在没有框架的情况下使用 React。 但需要确保,如果将它与框架一起使用,框架能够充分利用 React。 在过去几年中构建的许多功能(、useTransition、流式 API(如 renderToPipeableStream 和实验性的服务端组件)都是面向框架的。 它们让框架通过集成打包、路由和数据获取来充分利用 React。
可以看到,Next 13、Gatsby 5 和 Remix 1.11 中采用了其中一些功能。还有很多工作要做,其中一些工作正在从实验阶段结束。 尽管如此,React 团队还是很高兴看到多年的努力得到了回报,并使 React 框架(及其用户)能够发布更快的应用。

一个库,多个框架

React 生态系统更适合拥有众多竞争,有多种竞争的数据获取解决方案和路由解决方案。这些选择每年都会变得更好。也有多种集成路由、数据获取、渲染和编译的解决方案——即多个 React 框架。
我们希望保持这种状态,也希望在可能的情况下鼓励融合并使 React 生态系统从中受益。例如,不同的框架可能使用不同的机制来加载数据。但是,如果它们都采用 Suspense 作为加载指示器,那么在 Suspense 中的更高级别的功能将适用于所有框架。
如果大多数 React 应用的最佳方式是从一个框架开始,那我们应该建议使用哪个框架? 我们应该选择一个吗? 我们如何决定选择哪一个? 如果它随着时间的推移停滞不前怎么办? 这就引出了上面提到的问题。
我们应该用 Create React App 做什么?
Create React App 最初的目标是:
• 提供一种无需配置即可启动新 React 项目的简单方法;
• 集成编译相关依赖,方便升级;
• 让 React 团队尽可能广泛地部署工具更新(例如快速刷新支持、Hooks lint 规则)。
然而,它不再满足最初的目标,即成为创建 React 应用的最佳方式。通过提高标准并将编译与渲染、路由和数据获取相集成,框架可以让用户创建 React 应用时:
• 充分利用 Web API 来默认提供快速的应用和网站,无论大小;
• 充分利用 React 及其框架级功能;
• 提供路由和数据获取。
React 生态系统值得推荐一种默认的方法,它可以充分利用 Web 和 React 本身。这甚至并不意味着一定要依赖于 Node.js 服务器。 许多流行的框架不需要服务器并且可以在 SSG 模式下工作,因此它们也可以解决“完全静态”的用例。 框架的好处就是,如果以后需要 SSR,不需要进行迁移,它和其他功能一样开箱即用(例如,Remix 提供了开箱即用的 mutation API)。
那该如何实现这一愿景?有以下选择:
选项 1:从头开始创建一个新框架
可以尝试将 Create React App 重新设计架构成为一个集成数据获取、路由、打包和 SSG/SSR 的框架。构建一个高质量的新框架是一项艰巨的任务,需要大量的生态系统专业知识,即使停止其他项目来实现这一目标,也会存在着随着时间的推移出现停滞不前的重大风险,就像 Create React App 一样。它还会进一步分裂生态系统,尽管没有真正的用户。 所以认为这个选项目前不实用。
选项 2:弃用 Create React App,维护一个Vite模板
可以弃用 Create React App,维护自己的 Vite 模板。 为了实现这个目标,该模板必须非常复杂。 事实上,它必须像 React 框架一样复杂——并且需要集成路由、数据获取等功能。这导致了同样的问题:实际上是在创建另一个框架。
选项 3:弃用 Create React App,建议使用 React 框架
可以不再强调或反对将 Create React App 作为工具,而是更积极地强调 React 框架。这并不意味着必须使用其中一个React框架,但建议在大多数应用中使用其中一个框架。不利的一面就是,这将影响 React 的品牌(“为什么不推荐创建 React 应用?”)。
选项 4:让 Create React App 使用单一框架
可以选择一个指定框架,并更改 Create React App 以默认使用该框架创建应用。这种方法的主要问题是它使其他解决方案很难竞争——尤其是当它们的权衡略有不同,但在流行度、功能集和质量方面大致相同时。 这种行为上的改变也是具有破坏性的,因为所有的旧教程都会以一种不明显的方式中断。
选项 5:将 Create React App 变成启动器
可以将 Create React App 保留为命令,但将其变成启动器。它将建议一个推荐框架列表,然后是“经典”无框架方法。“经典”方法将产生一个像 CRA 现在这样的客户端专用应用(以避免破坏已有教程),但内部最终可能会使用 Vite。
要想进入精选框架列表,React 框架必须满足特定条件。需要考虑社区的流行度和采用率(以保持列表简短)、功能集、性能特征、充分利用 Web 平台和 React 本身的能力、它是否得到积极维护以及是否清楚如何在各种托管服务和环境中托管它。每个框架的入门模板将由 React 团队维护,以确保它们具有一致的设计和品牌,不链接到商业服务,并且结构相似。需要向社区清楚地传达是如何做出这些选择的,并且会定期重新评估它们。

React 团队的建议

React 团队目前倾向于选项 5(“将 Create React App 变成启动器”)。 Create React App 的最初目标是为大多数 React 用户提供启动新的 React web 应用的最佳方式。重新调整它的用途,启动器明确传达了我们认为最适合大多数新 Web 应用的转变。与选项 3 不同,它避免了“创建一个 React 应用”在某种程度上被弃用的看法。
React 团队将制定更详细的 RFC 提案,以充实这些要点。同时,希望听到对这些问题的更多反馈。
对于使用 Vite 替换 Create React App, 你有什么看法?欢迎在评论区分享~
参考:github.com/reactjs/rea…

09
7

Create React App(以下简称 CRA)是一个官方支持的创建 React 单页应用的脚手架,它提供了一个零配置的现代构建设置,将一些复杂工具(比如 webpack, Babel)的配置封装了起来,让使用者不用关心这些工具的具体配置,从而降低了工具的使用难度。

创建方法

npx: npx 来自 npm 5.2+ 或更高版本

npx create-react-app my-app
npm: npm init 在 npm 6+ 中可用

npm init react-app my-app
Yarn: yarn create 在 Yarn 0.25+ 中可用

yarn create react-app my-app
Scripts
在新创建的项目中,你可以运行一些内置命令:

npm start 或 yarn start
在开发模式下运行应用程序, 默认在浏览器打开http://localhost:3000。如果更改代码,页面将自动重新加载。

npm test 或 yarn test
以交互模式运行测试程序。 默认情况下,运行与上次提交后更改的文件相关的测试。

npm run build 或 yarn build
将生产环境的应用程序构建到 build 目录。 它能将 React 正确地打包为生产模式中并优化构建以获得最佳性能。构建将被压缩,文件名中将包含哈希。

npm run eject
注意:这是单向操作。一旦 eject ,就回不去了!
执行完这个命令后会将封装在 CRA 中的配置全部反编译到当前项目,这样开发者完全取得 webpack 文件的控制权,可以自定义修改webpack打包配置。

默认文件结构

创建后,项目文件结构如下所示:

my-app
node_modules
public
favicon.ico
index.html // 页面模板
manifest.json
src
App.css
App.js
App.test.js
index.css
index.js // 项目入口
logo.svg
reportWebVitals.js
setupTests.js
.gitignore
package.json
README.md
yarn.lock
为了加快重新构建的速度,Webpack 只处理 src 中的文件。 你需要将 JS 和 CSS 文件放在 src 中,否则 Webpack 将发现不了。
只能在 public/index.html 中使用 public 中的文件。
manifest.js: 将Web应用程序安装到设备的主屏幕,为用户提供更快的访问和更丰富的体验。

项目升级

Create React App 分为两个包:

create-react-app 是一个全局命令行实用程序,可用于创建新项目。
react-scripts 包含Create React App的脚本与配置
当你运行 create-react-app 时,它始终使用最新版本的 react-scripts 创建项目,新创建的应用会获得所有新功能和改进。

CRA 将所有新特性委托给 react-scripts , 只需要更新 react-scripts, 不需要更新 create-react-app 就可以升级CRA 的特性。比如用老版本 CRA 创建了一个项目,这个项目不具备 PWA 功能,但只要项目升级了 react-scripts 包的版本就可以具备 PWA 的功能,项目本身的代码不需要做任何修改。

如何在启动项目时不清空 terminal?
在运行 yarn start 或 npm start 来启动 create-react-app 创建的项目时,默认会清空 terminal。实际在进行开发调试时,需要在 webpack 中输出一些信息。

解决方法:
1、修改node_modules/react-dev-utils/clearConsole.js 文件:

'use strict';

function clearConsole() {
if (process.env.REACT_APP_NO_CLEAR_CONSOLE) {
return;
}
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
}

module.exports = clearConsole;
2、修改 package.json文件,将变量 REACT_APP_NO_CLEAR_CONSOLE 添加到script命令中:

// package.json
{
"scripts": {
"start": "REACT_APP_NO_CLEAR_CONSOLE=true react-app-rewired start",
}
}
如何扩展 Create React App 的 Webpack 配置
Create React App已经封装了webpack 配置,如果想对 webpack 配置做一些修改,这个时候应该怎么办呢?CRA提供了以下几种方式来修改 webpack 的配置:

eject 命令

替换 react-scripts 包
使用 react-app-rewired
scripts 包 + override 组合
customize-cra 【推荐】
eject 命令
使用 CRA 创建完项目以后,在package.json里面提供了这样一个命令:

{
...
"scripts": {
"eject": "react-scripts eject"
},
...
}
执行yarn eject后会将封装在 CRA 中的配置全部复制到当前项目。eject 后项目根目录下会新增 config与scripts 文件夹,修改package.json与yarn.lock文件。

config
jest
env.js
getHttpsConfig.js
modules.js
paths.js
pnpTs.js
webpack.config.js
webpackDevServer.config.js
scripts
build.js
start.js
test.js
如果使用了eject命令,虽然扩展了 webpack 配置,但是再也享受不到 CRA 升级带来的好处了。因为react-scripts已经是以文件的形式存在于你的项目,而不是以包的形式,所以无法对其升级。

替换 react-scripts 包

react-scripts 是 CRA 的一个核心包,一些脚本和工具的默认配置都集成在里面,使用 CRA 创建项目默认就是使用这个包。但是 CRA 还提供了另外一种方式来创建 CRA 项目,用自定义 scripts 包的方式。

// 默认方式
$ create-react-app my-app

// 自定义 scripts 包方式
$ create-react-app my-app --scripts-version 自定义包
自定义包可以是下面几种形式:

react-scripts包的版本号,比如0.8.2,这种形式可以用来安装低版本的react-scripts包。
一个已经发布到 npm 仓库上的包的名字,比如your-scripts,里面包含了修改过的 webpack 配置。
一个 tgz 格式的压缩文件,比如/your/local/scripts.tgz,通常是未发布到 npm 仓库的自定义 scripts 包,可以用 npm pack 命令生成。
这种方式相对于之前的eject是一种更灵活地修改 webpack 配置的方式,而且可以做到和 CRA 一样,通过升级 scrips 包来升级项目特性。
自定义 scripts 包的结构可以参照react-scripts包的结构,只要修改对应的 webpack 配置文件,并安装上所需的 webpack loader 或 plugin 包就可以。

使用 react-app-rewired
react-app-rewired 是 react 社区开源的一个修改 CRA 配置的工具,这种方式让开发者既不用eject项目也不用自己创建 scripts 包就可以拓展webpack。

如何使用

1.在 CRA 创建的项目中安装react-app-rewired

npm install react-app-rewired --save-dev
2.在项目根目录下创建config-overrides.js 文件(支持自定义文件路径)

/ config-overrides.js /

module.exports = function override(config, env) {
// 参数中的 config 就是默认的 webpack config

// 对 config 进行任意修改
config.mode = 'development';

// 一定要把新的 config 返回
return config;
}
config-overriders.js 导出的是一个函数,这个函数的签名是 const override = (oldWebpackConfig, env) => newWebpackConfig。

3.修改 scripts 命令:

/ package.json /

"scripts": {

  • "start": "react-scripts start",
  • "start": "react-app-rewired start",
  • "build": "react-scripts build",
  • "build": "react-app-rewired build",
  • "test": "react-scripts test",
  • "test": "react-app-rewired test",
    "eject": "react-scripts eject"
    }
    自定义 config-overrides.js 文件路径
    通过package.json 的config-overrides-path设置自定义路径:

/ package.json /
{
...
"config-overrides-path": "src/app", // src/app/config-overrides.js
...
}
config-overrides.js 文件
默认情况下,config-overrides.js文件导出一个函数,这个函数的签名是 const override = (oldWebpackConfig, env) => newWebpackConfig(oldWepbackConfig 和 newWebpackConfig 实际指向同一个对象,因为直接在原来的 webpack config 对象上进行修改)。

也可以改为此文件导出一个对象,该对象最多包含三个字段,每个字段都是一个函数。

module.exports = {
// The Webpack config
webpack: function(config, env) {
// ...add your webpack config
return config;
},
// The Jest config
jest: function(config) {
// ...add your jest config customisation...
return config;
},
// create a webpack dev server
devServer: function(configFunction) {
return function(proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
const fs = require('fs');
config.https = {
key: fs.readFileSync(process.env.REACT_HTTPS_KEY, 'utf8'),
cert: fs.readFileSync(process.env.REACT_HTTPS_CERT, 'utf8'),
ca: fs.readFileSync(process.env.REACT_HTTPS_CA, 'utf8'),
passphrase: process.env.REACT_HTTPS_PASS
};
return config;
};
},
paths: function(paths, env) {
// ...add your paths config
return paths;
},
}

实现原理实现原理

编译时,react-app-rewired 会先取到 create-react-app 生成的默认的 webpack config,然后调用 override(config) 方法,对 config 进行修改,得到新的 webpack config。webpack 最终会使用这个新的 config 进行打包。

流程大致如下:

const overrides = require('../config-overrides');
const webpackConfigPath = paths.scriptVersion + "/config/webpack.config.prod";

// load original config
const webpackConfig = require(webpackConfigPath);

// override config in memory
require.cache[require.resolve(webpackConfigPath)].exports =
overrides.webpack(webpackConfig, process.env.NODE_ENV);

// run original script
require(paths.scriptVersion + '/scripts/build');
scripts 包 + override 组合
虽然 react-app-rewired 的方式已经可以很方便地修改 webpack 的配置了,但也可以在自定义的 script 包中实现类似的功能。

以 build.js 为例,在获取基本 webpack 配置对象和使用 webpack 对象之间加入以下代码:

// override config
const override = require(paths.configOverrides);
const overrideFn = override || ((config, env) => config);
const overrideConfig = overrideFn(config, process.env.NODE_ENV);
overrideConfig 就是修改后的 webpack 对象,最后修改调用了 webpack 对象的代码,将原来的 webpack 对象替换成修改后的 webpack 对象。

customize-cra
react-app-rewired 原生写法,对 webpack config 的修改全部写在 override() 一个方法中,不够模块化。customized-cra 提供了一些 helper 方法,可以将每一个独立的修改放到单独的函数中,再串行执行这些函数。
customize-cra 依赖于 react-app-rewired 库,通过 config-overrides.js 来修改底层的 webpack,babel等配置。

安装
yarn add customize-cra react-app-rewired --dev

使用
customize-cra 导出 customizers 和 utilities 两种类型的api,查看 api docs了解更多。

customizers: 是对配置对象进行修改的方法, 让用户轻松启用或禁用 webpack,webpack-dev-server,babel等功能。
utilities: 定制一些方法用于浏览其配置。
所有代码都应写在 config-overrides.js 文件中。
参阅 api.md 文档获取有关 customize-cra 提供的功能。
const {
override,
addWebpackAlias,
} = require("customize-cra");
const path = require("path");

module.exports = override(
// add an alias for "page" imports
addWebpackAlias({
page: path.resolve(__dirname, "src/page")
}),
);
override()
新的 override() 方法,是一个高阶函数,接受可变数量的参数,每个参数都是签名为 const fn = (oldConfig) => newConfig 的函数;同时会返回一个新的函数,这个函数的签名也是 const fn = (oldConfig) => newConfig。

override() 会在内部依次调用传入的参数函数,把前一个函数返回的 newConfig 作为后一个函数的 oldConfig 参数,得到最终的 webpack config。

大致实现如下:

function override(fns) {
return function (oriConfig) {
let finalConfig = oriConfig
for (const fn of fns) {
finalConfig = fn(finalConfig)
}
return finalConfig
}
}
如何添加文件别名?
通过 addWebpackAlias,添加文件别名。

// config-overrides.js
const { override, addWebpackAlias } = require('customize-cra')
const path = require("path")

module.exports = override(
addWebpackAlias({
"components": path.resolve(__dirname, "src/components")
}),
)
fixBabelImports()
babel 模块化导入插件。具体查看 babel-plugin-import

const { override, fixBabelImports } = require("customize-cra");

module.exports = override(
fixBabelImports('antd', {
"libraryName": "antd",
libraryDirectory: 'es',
"style": true,
})
);
style
style: true 导入CSS源文件,在编译期间进行优化。可以明显减小分发包的大小,具体取决于对库的使用情况。
style: css 将预捆绑的CSS文件直接导入。

如何自定义 babel?
通过 useBabelRc,实现自定义 babel。

// config-overrides.js
const { override, useBabelRc } = require('customize-cra')

module.exports = override(
useBabelRc(),
)
项目根目录新建 babelrc文件:

// .babelrc
{
"presets": ["@babel/env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
如何添加插件?
通过 addWebpackPlugin,添加插件。

// config-overrides.js
const { override, addWebpackPlugin } = require('customize-cra')
const DefinePlugin = require('webpack').DefinePlugin
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = override(
addWebpackPlugin(
new DefinePlugin({
'test': 12244,
}),
),
addWebpackPlugin(new BundleAnalyzerPlugin()),
)
如何 SplitChunks?
通过 setWebpackOptimizationSplitChunks,自定义拆包。

// config-overrides.js
const { override, setWebpackOptimizationSplitChunks } = require('customize-cra')

module.exports = override(
setWebpackOptimizationSplitChunks({
maxSize: 1024 1024 3,
minChunks: 2,
})
)

26
6

MiniProfiler工具介绍

0
归档:2024年6月分类:C#和.NET

在日常开发中,应用程序的性能是我们需要关注的一个重点问题。当然我们有很多工具来分析程序性能:如:Zipkin等;但这些过于复杂,需要单独搭建。

MiniProfiler就是一款简单,但功能强大的应用新能分析工具;可以帮助我们定位:SQL性能问题、响应慢等问题。

MiniProfiler是一款针对.NET, Ruby, Go and Node.js的性能分析的轻量级程序。可以对一个页面本身,及该页面通过直接引用、Ajax、Iframe形式访问的其它页面进行监控,监控内容包括数据库内容,并可以显示数据库访问的SQL(支持EF、EF CodeFirst等 )。并且以很友好的方式展现在页面上。

MiniProfiler官网:http://miniprofiler.com/

MiniProfiler的一个特别有用的功能是它与数据库框架的集成。除了.NET原生的 DbConnection类,MiniProfiler还内置了对实体框架(Entity Framework)以及LINQ to SQL、RavenDb和MongoDB的支持。任何执行的Step都会包括当时查询的次数和所花费的时间。为了检测常见的错误,如N+1反模式,profiler将检测仅有参数值存在差异的多个查询。

25
6

作者 Julie Lerman

你大概注意到了,自 2008 年以来,我写过许多关于 Entity Framework(即 Microsoft 对象关系映射器 (ORM))的文章,ORM 一直是主要的 .NET 数据访问 API。市面上还有许多其他 .NET ORM,但是有一个特殊类别因其强大的性能得到的关注最高,那就是微型 ORM。我听人提到最多的微型 ORM 是 Dapper。据不同的开发者说,他们使用 EF 和 Dapper 制定了混合解决方案,让每个 ORM 能够在单个应用程序中做它最擅长的事,这最终激发了我的兴趣,促使我在最近抽出时间来一探究竟。

在阅读大量文章和博客文章,与开发者聊过天并熟悉过 Dapper 后,我想与大家分享我的一些发现,尤其是和像我这样,可能听说过 Dapper 但并不知道它是什么或者并不知道它的工作原理的人分享,同时说说人们为什么这么喜欢它。需要提醒你的是,我根本不是什么专家。目前我只是为了满足我的好奇心而变得足够了解,并且希望激发你的兴趣,从而进一步探索。

为什么是 Dapper?
Dapper 的历史十分有趣,它是从你可能再熟悉不过的资源中衍生的: Marc Gravell 和 Sam Saffron 在研究 Stack Overflow,解决此平台的性能问题时构建了 Dapper。考虑到 Stack Overflow 是一个流量极高的站点,那么必然存在性能上的问题。根据 Stack Exchange About 网页,在 2015 年,Stack Overflow 拥有 57 亿的网页浏览量。在 2011 年,Saffron 撰写过一篇关于他和 Gravell 所做的工作的博客文章,名为“我如何学会不再担忧和编写我自己的 ORM”(bit.ly/),这篇文章介绍了 Stack 当时存在的性能问题,该问题源于 LINQ to SQL 的使用。他在文中详细介绍了为什么编写自定义 ORM,其中 Dapper 就是优化 Stack Overflow 上的数据访问的答案。五年后的今天,Dapper 已被广泛使用并已成为开源软件。Gravell 和 Stack 及团队成员 Nick Craver 继续在 github.com/StackExchange/dapper-dot-net 上积极地管理项目。

Dapper 简介
Dapper 主要能够让你练习你的 SQL 技能,按你认为的那样构建查询和命令。它接近于“金属”而非标准的 ORM,免除了解释查询的工作,例如将 LINQ to EF 解释为 SQL。Dapper 不具备炫酷的转换功能,比如打散传递到 WHERE IN 从句的列表。但在大多数情况下,你发送到 Dapper 的 SQL 已准备好运行,而查询可以更快地到达数据库。如果你擅长 SQL,那么你将有把握编写性能最高的命令。你需要创建某些类型的 IDbConnection 来执行查询,比如带有已知连接字符串的 SqlConnection。然后,Dapper 可以通过其 API 为你执行查询以及—假如查询结果的架构与目标类型的属性相匹配—自动实例化对象并向对象填充查询结果。此处还有另一个显著的性能优势: Dapper 能够有效缓存它获悉的映射,从而实现后续查询的极速反序列化。我将填充的类 DapperDesigner(如图 1 中所示)被定义用来管理构建整齐构架的设计器。

图 1 DapperDesigner 类

C#

复制
public class DapperDesigner
{
  public DapperDesigner() {
    Products = new List<Product>();
    Clients = new List<Client>();
  }
  public int Id { get; set; }
  public string LabelName { get; set; }
  public string Founder { get; set; }
  public Dapperness Dapperness { get; set; }
  public List<Client> Clients { get; set; }
  public List<Product> Products { get; set; }
  public ContactInfo ContactInfo { get; set; }
}

我执行查询的项目引用了我通过 NuGet 获取的 Dapper(安装包 dapper)。下面是从 Dapper 调用以为 DapperDesigners 表中所有行执行查询的示例:

C#

复制
var designers = sqlConn.Query<DapperDesigner>("select * from DapperDesigners");

需要注意的是,对于本文中的代码清单,当我希望使用表中的所有列时,我使用的是 select * 而非明确投影的查询列。sqlConn 连同其连接字符串是现有的实例化 SqlConnection 对象,但是尚未打开过。

Query 方法是 Dapper 提供的扩展方法。在此行执行时,Dapper 打开连接,创建 DbCommand,准确地按照我编写的内容执行查询,实例化结果中的每行的 DapperDesigner 对象并将值从查询结果推送到对象的属性。Dapper 可以通过几种方式将结果值与属性进行匹配,即使属性名称与列名称不相匹配,又或者即使属性的顺序与匹配的列的顺序不同。它不会读心术,所以别期望它弄清涉及的映射,例如列的顺序或名称和属性不同步的大量字符串值。我确实用它做了几个奇怪的实验,我想看看它如何响应,同时我也配置了控制 Dapper 如何推断映射的目标设置。

Dapper 和关系查询
我的 DapperDesigner 类型拥有多种关系,比如一对多(与产品)、一对一 (ContactInfo) 和多对多(客户端)。我已经试验过跨这些关系执行查询,而且 Dapper 能够处理这些关系。这绝对不像使用 Include 方法或投影表述 LINQ to EF 查询那么简单。我的 TSQL 技能被推到极限,这是因为 EF 在过去几年让我变得如此懒惰。

下面是使用我在数据库中使用 SQL 进行跨一对多关系的查询的示例:


C#

复制
var sql = @"select * from DapperDesigners D
           JOIN Products P
           ON P.DapperDesignerId = D.Id";
var designers= conn.Query<DapperDesigner, Product,DapperDesigner>
(sql,(designer, product) => { designer.Products.Add(product);
                              return designer; });

注意 Query 方法要求我指定两种必须构建的类型,并指示要返回的类型—由最终类型参数 (DapperDesigner) 表述。我首先使用多行匿名函数构建图表,将相关产品添加到其父设计器对象,然后将每个设计器返回到 Query 方法返回的 IEnumerable。

通过我对 SQL 的最佳尝试,这样做的不利之处在于结果是扁平的,就像使用 EF Include 方法时一样。每个产品我将获取一行并复制一下设计器。Dapper 拥有可以返回多个结果集的 MultiQuery 方法。与 Dapper 的 GridReader 组合,这些查询的性能肯定将胜过 EF Includes。

编码难度加大,执行速度变快
表述 SQL 并填充相关对象是我让 EF 在此背景中处理的任务,所以需要更多精力来编码。但是如果你要处理的数据量很大,那么运行时性能非常重要,这当然值得努力。在我的示例数据库中拥有 30,000 个设计器。仅有几个拥有产品。我做了一些简单的基准测试,确保我所做的是同类比较。在查看测试结果前,有些关于我如何测量的重点需要大家理解。

请记住,默认情况下,EF 的设计目的是跟踪为查询结果的对象。这意味着它创建了额外的跟踪对象(需要做一些工作),并且它也需要与这些跟踪对象互动。而 Dapper 只是将结果转储到内存。所以当进行性能对比时,让 EF 的更改跟踪不再循环非常重要。为此,我使用 AsNoTracking 方法定义我的所有 EF 查询。同时,当对比性能时,你需要应用大量的标准基准模式,比如给数据库热身、反复执行查询以及抛弃最慢时间和最快时间。你可以看到我如何在下载示例中构建我的基准测试的详情。我仍然认为这些测试是“轻量级”基准测试,此处只是为了展现差异。对于较高的基准,你需要多次迭代(500 次以上),而我只进行了 25 次,这是远远不够的,同时还需要将你运行的系统的性能考虑在内。我在笔记本上使用 SQL Server LocalDB 实例进行这些测试,所以我的结果仅用于比较。

我在测试中跟踪的的时间为执行查询和构建结果的时间。未计入实例化连接或 DbContexts 的时间。因为反复使用 DbContext,所以构建内存内模型的时间不计入内,因为每个应用程序示例仅构建一次,而不是每个查询都要构建。

图 2 显示了 Dapper 和 EF LINQ 查询的“select *”测试,从中你可以看到我的测试模式的基本构造。注意,除收集实际时间外,我还在收集每次迭代的时间并整理到列表(名为“时间”)中以供进一步分析。

图 2 查询所有 DapperDesigners 时 EF 与 Dapper 的对比测试

C#

复制
[TestMethod,TestCategory("EF"),TestCategory("EF,NoTrack")]
public void GetAllDesignersAsNoTracking() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var context = new DapperDesignerContext()) {
      _sw.Reset();
      _sw.Start();
      var designers = context.Designers.AsNoTracking().ToList();
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _trackedObjects = context.ChangeTracker.Entries().Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}
[TestMethod,TestCategory("Dapper")
public void GetAllDesigners() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var conn = Utils.CreateOpenConnection()) {
      _sw.Reset();
      _sw.Start();
      var designers = conn.Query<DapperDesigner>("select * from DapperDesigners");
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _retrievedObjects = designers.Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}

关于同类对比,还有一个问题。 Dapper 使用原始 SQL。默认情况下,使用 LINQ to EF 表述 EF 查询并且必须做一些工作才能为你构建 SQL。一旦构建好 SQL,即使是依靠参数的 SQL,它将被缓存到应用程序的内存,以减少重复工作。此外,EF 可以使用原始 SQL 执行查询,所以我考虑到了这两种方法。图 3 列出了四组测试的对比结果。下载包含更多测试。

图 3 基于 25 次迭代执行查询和填充对象的平均时间(以毫秒计),排除最快和最慢时间

AsNoTracking 查询 关系 LINQ to EF EF Raw SQL Dapper Raw SQL
所有设计器(3 万行) – 96 98 77
所有带产品的设计器(3 万行) 1 :
251 107 91
所有带客户端的设计器(3 万行) : 255 106 63
所有带联系人的设计器(3 万行) 1 : 1 322 122 116

在 图 3 显示的场景中,我们可以很容易地跨 LINQ to Entities 使用 Dapper 制作一个案例。但是原始 SQL 查询之间的细微差异可能不总是在使用 EF 的系统中为特定任务切换到 Dapper 的正当理由。理所当然,大家的需求各有不同,所以这可能影响 EF 查询和 Dapper 之间的差异程度。但是,在 Stack Overflow 等高流量系统中,甚至是每个查询保存的大量毫秒时间都可能至关重要。

用于其他暂留需求的 Dapper 和 EF
到目前为止,我测量了简单查询,并在其中从所返回类型的准确匹配属性的表中回拉所有列。如果你将查询投影到类型会如何呢? 只要结果构架与类型相匹配,Dapper 将无法观察到创建对象的差异。但是,如果投影结果与为属于模型一部分的类型不一致,EF 不得不多做些工作。

DapperDesignerContext 拥有一个针对 DapperDesigner 类型的 DbSet。在我的系统中有另一个名为 MiniDesigner 的类型,它拥有一个 DapperDesigner 属性的子集。

C#

复制
public class MiniDesigner {
    public int Id { get; set; }
    public string Name { get; set; }
    public string FoundedBy { get; set; }
  }

MiniDesigner 不属于我的 EF 数据模型,所以 DapperDesigner­Context 不知道这种类型。我发现与使用借用原始 SQL 的 EF 相比,使用 Dapper 查询所有这 30,000 行并将其投影到 30,000 个 MiniDesigner 对象要快 25%。我再次建议你做自己的性能分析,并为你自己的系统做出决策。

Dapper 也可用于将数据推送到数据库,其中包含允许你识别必须用于命令指定参数的属性的方法,不论你使用的是原始 INSERT 或 UPDATE 命令,还是对数据库执行函数或存储过程。我并没有对这些任务做任何性能对比。

现实世界中的混合 Dapper 和 EF
有许多将 Dapper 用于 100% 数据暂留的系统。但是回忆起来,我的兴趣是由谈论混合解决方案的开发者激起的。在某些情况下,还存在已有 EF 并希望微调特定问题区域的系统。在其他情况下,团队选择使用 Dapper 执行所有查询,使用 EF 执行所有保存。

有人回复了我在 Twitter 上发布的关于这方面的问题,答案千变万化。

@garypochron 告诉我他的团队“将 Dapper 用于高需区域并使用资源文件维护 SQL 的组织。“ 而热门 EF Reverse POCO Generator 的作者 Simon Hughes (@s1monhughes) 的习惯恰好相反—默认使用 Dapper,遇到棘手问题时则使用 EF,对此我感到很吃惊。他告诉我“只要可能,我都会使用 Dapper。如果是比较复杂的更新,我会使用 EF。”

我也见过各种混合方法是由于要分离关注点而非提高性能而推动的讨论。最常见的讨论是利用 EF 上的 ASP.NET Identity 的默认依赖性,然后在解决方案中使用 Dapper 进行其余存储。

除性能外,更直接地处理数据库还拥有其他优点。SQL Server 专家 Rob Sullivan (@datachomp) 和 Mike Campbell (@angrypets) 也对 Dapper 青睐有加。Rob 指出你可以利用 EF 不允许访问的数据库功能,比如全文搜索。从长期来看,特殊功能是关于性能的。

另一方面,有些任务只能使用 EF 完成,使用 Dapper 根本完成不了(更改跟踪除外)。一个很好的例子是我在构建为本文创建的解决方案时利用的功能—即使用 EF Code First Migrations 在模型更改时迁移数据库的能力。

Dapper 并不适合每一个人。@damiangray 告诉我 Dapper 不是他的解决方案之选,因为他需要能够将 IQueryables(不是真实数据)从系统的一部分返回到另一部分。这个推迟执行查询的主题已在 Dapper 的 GitHub 存储库中提出,如果你想详细了解此主题,请访问 bit.ly/22CJzJl。在设计混合系统时,使用 Command Query Separation (CQS) 是个不错的方法,你可以在其中为特定类型的交易设计独立的模型,至少我着迷于此。这样一来,你不必设法去构建普通的数据访问代码以使用 EF 和 Dapper,因为构建此代码通常会牺牲每个 ORM 的好处。在我创作这篇文章时,Kurt Dowswell 发布了一篇名为“Dapper、EF 和 CQS”(bit.ly/1LEjYvA) 的博文。对我来说得心应手,对你来说亦是如此。

对于那些期待 CoreCLR 和 ASP.NET Core 的人来说,Dapper 已演变为能够支持这些功能的软件。你可以在 Dapper 的 GitHub 存储库中的文章 (bit.ly/1T5m5Ko) 中找到更多信息。

最后,我看了看 Dapper。我认为怎么样?
我怎么样? 我很遗憾没能尽早正视 Dapper,同时也因最终实现了愿望而感到很高兴。我始终推荐 AsNoTracking 或建议使用数据库中的视图或过程缓解性能问题。它从未让我或我的客户失望过。但是现在我知道我还有另一招妙计要推荐给对从使用 EF 的系统中榨出更多性能感兴趣的开发者。这不是我们所谓的稳操胜券。我的建议将用来探索 Dapper、测量性能差异(大规模)以及找到性能与编码难度之间的平衡点。想想 StackOverflow 的显著用途:查询问题、注释和答案,然后连同一些元数据(编辑)和用户信息返回附有注释和答案的问题图表。它们反复执行相同类型的查询并标绘出相同形状的结果。Dapper 的设计更擅长这种类型的反复查询,并且每次都会变得更智能、更快速。即使你的系统中没有设计为供 Dapper 处理的海量交易,你也可能找到满足你需求的混合解决方案。

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 .NET 主题的演示。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。通过 Twitter 关注她:@julielerman 并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。

衷心感谢以下 Stack Overflow 技术专家对本文的审阅: Nick Craver 和 Marc Gravell
Nick Craver (@Nick_Craver) 既是开发者,又是网站可靠性工程师,同时兼职 Stack Overflow 的 DBA。他擅长各层、总体系统体系结构和数据中心硬件的性能调节以及 Opserver 等开源项目的维护。访问 了解他的相关信息。

Marc Gravell 是 Stack Overflow 的开发者,主要专注于高性能库和 .NET 工具,尤其是数据访问、序列化、网络 API,为这些领域的一系列开源项目做出了贡献。

23
6

Entity Framework Core:一对多关系

0
归档:2024年6月分类:C#和.NET

当一个实体类型的任意数量的实体与相同或另一个实体类型的任意数量的实体相关联时,将使用多对多关系。 例如,Post 可以有多个关联的 Tags,并且每个 Tag 可以与任意数量的 Posts 关联。

了解多对多关系
多对多关系不同于一对多和一对一关系,因为它们不能仅使用外键以简单方式表示。 相反,需要其他实体类型来“联接”关系的两端。 这称为“联接实体类型”,并映射到关系数据库中的“联接表”。 此联接实体类型的实体包含外键值对,其中一对指向关系一端的实体,另一对指向关系另一端的实体。 因此,每个联接实体以及联接表中的每一行都表示关系中实体类型之间的一个关联。

EF Core 可以隐藏联接实体类型并在后台对其进行管理。 这允许以自然方式使用多对多关系的导航,从而根据需要在每一端添加或删除实体。 但是,了解后台发生的情况非常有用,以便其整体行为(尤其是映射关系数据库)有意义。 让我们从关系数据库架构设置开始,以表示帖子和标记之间的多对多关系:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

在此架构中,PostTag 是联接表。 它包含两列:PostsId(指向 Posts 表的主键的外键)和 TagsId(指向 Tags 表的主键的外键)。 因此,此表中的每一行都表示一个 Post 和一个 Tag 之间的关联。

EF Core 中此架构的简单映射由三种实体类型组成(每个表对应一个实体类型)。 如果其中每个实体类型都由 .NET 类表示,则这些类可能如下所示:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

请注意,在此映射中,没有多对多关系,而是存在两个一对多关系,一个用于联接表中定义的每个外键。 这不是映射这些表的不合理方法,但并不反映联接表的意图,即表示单个多对多关系,而不是两个一对多关系。

EF 允许通过引入两个集合导航实现更自然的映射:一个在包含其相关的 Tags 的 Post 上,另一个在包含其相关的 Posts 的 Tag 上。 例如:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = [];
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostsId { get; set; }
    public int TagsId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

提示

这些新导航称为“跳过导航”,因为它们跳过联接实体以提供对多对多关系另一端的直接访问。

如下面的示例所示,可以通过这种方式映射多对多关系,即使用联接实体的 .NET 类,使用两个一对多关系的导航和跳过实体类型上公开的导航。 但是,EF 可以透明地管理联接实体,而无需为其定义 .NET 类,且无需两个一对多关系的导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

事实上,默认情况下,EF 模型生成约定会将此处显示的 Post 和 Tag 类型映射到本节顶部的数据库架构中的三个表。 此映射(不显式使用联接类型)通常是指术语“多对多”。

示例
以下部分包含多对多关系的示例,包括实现每个映射所需的配置。

提示

可在 ManyToMany.cs 中找到以下所有示例的代码。

基本多对多
在多对多的最基本情况下,关系两端的实体类型都具有集合导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

此关系按约定映射。 即使不需要,此关系的等效显式配置也以学习工具的形式显示如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts);
}

即使使用此显式配置,关系的许多方面仍按约定进行配置。 出于学习目的,更完整的显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            "PostTag",
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagsId").HasPrincipalKey(nameof(Tag.Id)),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostsId").HasPrincipalKey(nameof(Post.Id)),
            j => j.HasKey("PostsId", "TagsId"));
}

重要

即使不需要,也不要尝试完全配置所有内容。 如上所示,代码很快就变得复杂,并且容易出错。 即使在上面的示例中,模型中有许多内容仍按约定进行配置。 认为 EF 模型中的所有内容始终可以完全显式配置是不现实的。

无论关系是按约定还是使用所示的任一显式配置生成,生成的映射架构(使用 SQLite)都是:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

使用 Database First 流从现有数据库搭建 DbContext 的基架时,EF Core 6 及更高版本会在数据库架构中查找此模式,并按本文档中所述搭建多对多关系的基架。 可以使用自定义 T4 模板更改此行为。 有关其他选项,请参阅无映射联接实体的多对多关系现已搭建基架。

重要

目前,EF Core 使用 Dictionary<string, object> 表示未配置 .NET 类的联接实体实例。 但是,为了提高性能,将来的 EF Core 版本中可能会使用另一种类型。 除非已显式配置,否则不要依赖于联接类型 Dictionary<string, object>。

具有命名联接表的多对多
在前面的示例中,联接表按约定命名为 PostTag。 可以使用 UsingEntity 为它指定显式名称。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity("PostsToTagsJoinTable");
}

映射的其他所有内容都保持不变,只更改了联接表的名称:

CREATE TABLE "PostsToTagsJoinTable" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostsToTagsJoinTable" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostsToTagsJoinTable_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostsToTagsJoinTable_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有联接表外键名称的多对多
在前面的示例中,还可以更改联接表中外键列的名称。 可通过两种方式来执行此操作。 第一种方式是在联接实体上显式指定外键属性名称。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagForeignKey"),
            r => r.HasOne(typeof(Post)).WithMany().HasForeignKey("PostForeignKey"));
}

第二种方式是保留属性的按约定名称,但将这些属性映射到不同的列名。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.Property("PostsId").HasColumnName("PostForeignKey");
                j.Property("TagsId").HasColumnName("TagForeignKey");
            });
}

在任一情况下,映射都保持不变,只更改了外键列名称:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

虽然此处未显示,但可以组合上述两个示例来映射更改联接表名称及其外键列名。

具有联接实体的类的多对多
到目前为止,在示例中,联接表已自动映射到共享类型实体类型。 这样就无需为实体类型创建专用类。 但是,拥有此类可能很有用,以便可以轻松引用它,尤其是在将导航或有效负载添加到类时,如后面的示例所示。 为此,除了 Post 和 Tag 的现有类型之外,首先为联接实体创建类型 PostTag:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

提示

类可以具有任何名称,但通常会在关系的任一端合并类型的名称。

现在,UsingEntity 方法可用于将其配置为关系的联接实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

PostId 和 TagId 作为外键自动选取,并配置为联接实体类型的复合主键。 对于与 EF 约定不匹配的情况,可以显式配置用于外键的属性。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany().HasForeignKey(e => e.TagId),
            r => r.HasOne<Post>().WithMany().HasForeignKey(e => e.PostId));
}

此示例中联接表的映射数据库架构在结构上与前面的示例等效,但具有一些不同的列名:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有指向联接实体的导航的多对多
在前面的示例中,现在有一个表示联接实体的类,因此可以轻松添加引用此类的导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
}

重要

如本示例中所示,除了多对多关系两端之间的跳过导航外,还可以使用指向联接实体类型的导航。 这意味着,跳过导航可用于以自然方式与多对多关系进行交互,而当需要更好地控制联接实体本身时,可以使用指向联接实体类型的导航。 从某种意义上说,此映射在简单的多对多映射和更明确匹配数据库架构的映射之间提供了两者的最佳效果。

无需在 UsingEntity 调用中更改任何内容,因为指向联接实体的导航是按约定选取的。 因此,此示例的配置与上一个示例的配置相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

对于无法按约定确定导航的情况,可以显式配置导航。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>().WithMany(e => e.PostTags),
            r => r.HasOne<Post>().WithMany(e => e.PostTags));
}

在模型中包括导航不会影响映射的数据库架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有指向和来自联接实体的导航的多对多
前面的示例从多对多关系任一端的实体类型向联接实体类型添加了导航。 还可以在其他方向或两个方向添加导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

无需在 UsingEntity 调用中更改任何内容,因为指向联接实体的导航是按约定选取的。 因此,此示例的配置与上一个示例的配置相同:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

对于无法按约定确定导航的情况,可以显式配置导航。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags));
}

在模型中包括导航不会影响映射的数据库架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有导航和更改外键的多对多
前面的示例演示了具有指向和来自联接实体类型的导航的多对多。 此示例是相同的,只不过所使用的外键属性也会更改。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostForeignKey { get; set; }
    public int TagForeignKey { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

同样,UsingEntity 方法用于进行如下配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasForeignKey(e => e.TagForeignKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasForeignKey(e => e.PostForeignKey));
}

映射的数据库架构现在为:

CREATE TABLE "PostTag" (
    "PostForeignKey" INTEGER NOT NULL,
    "TagForeignKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostForeignKey", "TagForeignKey"),
    CONSTRAINT "FK_PostTag_Posts_PostForeignKey" FOREIGN KEY ("PostForeignKey") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagForeignKey" FOREIGN KEY ("TagForeignKey") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

单向多对多
备注

EF Core 7 中引入了单向多对多关系。 在早期版本中,可以将专用导航用作解决方法。

不必在多对多关系两端包含导航。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
}

EF 需要一些配置才能知道这应该是多对多关系,而不是一对多关系。 这是使用 HasMany 和 WithMany 完成的,但在没有导航的情况下,在一端不会传递参数。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany();
}

删除导航不会影响数据库架构:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有有效负载的多对多和联接表
到目前为止,在示例中,联接表仅用于存储表示每个关联的外键对。 但是,它还可用于存储有关关联的信息,例如,关联的创建时间。 在这种情况下,最好为联接实体定义类型,并将“关联有效负载”属性添加到此类型。 除了用于多对多关系的“跳过导航”之外,还经常创建指向联接实体的导航。 这些附加导航允许从代码中轻松引用联接实体,从而有助于读取和/或更改有效负载数据。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public DateTime CreatedOn { get; set; }
}

对有效负载属性使用生成的值也很常见,例如,插入关联行时自动设置的数据库时间戳。 这需要一些最小配置。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

结果映射到具有插入行时自动设置的时间戳的实体类型架构:

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

提示

此处显示的 SQL 适用于 SQLite。 在 SQL Server/Azure SQL 上,使用 .HasDefaultValueSql("GETUTCDATE()") 并用 TEXT 读取 datetime。

作为联接实体的自定义共享类型实体类型
前面的示例使用类型 PostTag 作为联接实体类型。 此类型特定于帖子-标记关系。 但是,如果有多个具有相同形状的联接表,则可以对所有这些表使用相同的 CLR 类型。 例如,假设所有联接表都有 CreatedOn 列。 可以使用映射为共享类型实体类型的 JoinType 类进行映射:

public class JoinType
{
    public int Id1 { get; set; }
    public int Id2 { get; set; }
    public DateTime CreatedOn { get; set; }
}

然后,可以通过多个不同的多对多关系将此类型引用为联接实体类型。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
    public List<JoinType> PostTags { get; } = [];
}

public class Blog
{
    public int Id { get; set; }
    public List<Author> Authors { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

public class Author
{
    public int Id { get; set; }
    public List<Blog> Blogs { get; } = [];
    public List<JoinType> BlogAuthors { get; } = [];
}

然后,可以适当配置这些关系,以便将联接类型映射到每个关系的不同表:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<JoinType>(
            "PostTag",
            l => l.HasOne<Tag>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id1),
            r => r.HasOne<Post>().WithMany(e => e.PostTags).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Authors)
        .WithMany(e => e.Blogs)
        .UsingEntity<JoinType>(
            "BlogAuthor",
            l => l.HasOne<Author>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id1),
            r => r.HasOne<Blog>().WithMany(e => e.BlogAuthors).HasForeignKey(e => e.Id2),
            j => j.Property(e => e.CreatedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

这会导致数据库架构中出现以下表:

CREATE TABLE "BlogAuthor" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_BlogAuthor" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_BlogAuthor_Authors_Id1" FOREIGN KEY ("Id1") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BlogAuthor_Blogs_Id2" FOREIGN KEY ("Id2") REFERENCES "Blogs" ("Id") ON DELETE CASCADE);

CREATE TABLE "PostTag" (
    "Id1" INTEGER NOT NULL,
    "Id2" INTEGER NOT NULL,
    "CreatedOn" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("Id1", "Id2"),
    CONSTRAINT "FK_PostTag_Posts_Id2" FOREIGN KEY ("Id2") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_Id1" FOREIGN KEY ("Id1") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

具有备用键的多对多
到目前为止,所有示例都演示将联接实体类型中的外键约束为关系任一端的实体类型的主键。 可以改为将每个外键约束为备用键。 例如,假设某个模型中的 Tag 和 Post 具有备用键属性:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
}

此模型的配置为:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasPrincipalKey(nameof(Tag.AlternateKey)),
            r => r.HasOne(typeof(Post)).WithMany().HasPrincipalKey(nameof(Post.AlternateKey)));
}

为清楚起见,生成的数据库架构还包括具有备用键的表:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostsAlternateKey" INTEGER NOT NULL,
    "TagsAlternateKey" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsAlternateKey", "TagsAlternateKey"),
    CONSTRAINT "FK_PostTag_Posts_PostsAlternateKey" FOREIGN KEY ("PostsAlternateKey") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsAlternateKey" FOREIGN KEY ("TagsAlternateKey") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

如果联接实体类型由 .NET 类型表示,则使用备用键的配置略有不同。 例如:

public class Post
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Tag> Tags { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public int AlternateKey { get; set; }
    public List<Post> Posts { get; } = [];
    public List<PostTag> PostTags { get; } = [];
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

配置现在可以使用泛型 UsingEntity<> 方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>(
            l => l.HasOne<Tag>(e => e.Tag).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey),
            r => r.HasOne<Post>(e => e.Post).WithMany(e => e.PostTags).HasPrincipalKey(e => e.AlternateKey));
}

生成的架构为:

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Posts_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "Tags" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
    "AlternateKey" INTEGER NOT NULL,
    CONSTRAINT "AK_Tags_AlternateKey" UNIQUE ("AlternateKey"));

CREATE TABLE "PostTag" (
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostId", "TagId"),
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("AlternateKey") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("AlternateKey") ON DELETE CASCADE);

具有单独主键的多对多和联接表
到目前为止,所有示例中的联接实体类型都具有由两个外键属性组成的主键。 这是因为这些属性的每个值组合最多可以出现一次。 因此,这些属性构成了自然主键。

备注

EF Core 不支持任何集合导航中的重复实体。

如果控制数据库架构,则联接表没有理由具有其他主键列。但是,现有联接表可能定义了主键列。 EF 仍可通过某些配置映射到此处。

最简单的方法也许是创建一个表示联接实体的类。 例如:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

public class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }
}

现在,此 PostTag.Id 属性按约定选取为主键,因此,需要的唯一配置是调用 PostTag 类型的 UsingEntity:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity<PostTag>();
}

联接表的结果架构为:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostId" INTEGER NOT NULL,
    "TagId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostId" FOREIGN KEY ("PostId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

还可以将主键添加到联接实体,而无需为其定义类。 例如,仅使用 Post 和 Tag 类型:

public class Post
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = [];
}

public class Tag
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = [];
}

可以使用以下配置添加键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            j =>
            {
                j.IndexerProperty<int>("Id");
                j.HasKey("Id");
            });
}

这会生成具有单独主键列的联接表:

CREATE TABLE "PostTag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_PostTag" PRIMARY KEY AUTOINCREMENT,
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE CASCADE);

无需级联删除的多对多
在上面显示的所有示例中,在联接表和多对多关系的两端之间创建的外键都是使用级联删除行为创建的。 这非常有用,因为这意味着如果删除关系任一端的实体,则会自动删除该实体的联接表中的行。 或者,换句话说,当实体不再存在时,它与其他实体的关系也不再存在。

很难想象更改此行为何时有用,但如果需要,可以执行此操作。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().OnDelete(DeleteBehavior.Restrict),
            r => r.HasOne(typeof(Post)).WithMany().OnDelete(DeleteBehavior.Restrict));
}

联接表的数据库架构对外键约束使用受限的删除行为:

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE RESTRICT,
    CONSTRAINT "FK_PostTag_Tags_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tags" ("Id") ON DELETE RESTRICT);

自引用多对多
可以在多对多关系的两端使用相同的实体类型;这称为“自引用”关系。 例如:

public class Person
{
    public int Id { get; set; }
    public List<Person> Parents { get; } = [];
    public List<Person> Children { get; } = [];
}

这映射到名为 PersonPerson 的联接表,其中两个外键都指向 People 表:

CREATE TABLE "PersonPerson" (
    "ChildrenId" INTEGER NOT NULL,
    "ParentsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PersonPerson" PRIMARY KEY ("ChildrenId", "ParentsId"),
    CONSTRAINT "FK_PersonPerson_People_ChildrenId" FOREIGN KEY ("ChildrenId") REFERENCES "People" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PersonPerson_People_ParentsId" FOREIGN KEY ("ParentsId") REFERENCES "People" ("Id") ON DELETE CASCADE);

对称自引用多对多
有时,多对多关系是自然对称的。 也就是说,如果实体 A 与实体 B 相关,则实体 B 也与实体 A 相关。这是使用单个导航自然建模的。 例如,假设人员 A 是人员 B 的朋友,则人员 B 是人员 A 的朋友:

public class Person
{
    public int Id { get; set; }
    public List<Person> Friends { get; } = [];
}

遗憾的是,这并不容易映射。 不能对关系的两端使用相同的导航。 最好是将其映射为单向多对多关系。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasMany(e => e.Friends)
        .WithMany();
}

但是,为了确保两个人彼此相关,需要手动将每个人添加到对方的 Friends 集合中。 例如:

ginny.Friends.Add(hermione);
hermione.Friends.Add(ginny);

直接使用联接表
上述所有示例都使用 EF Core 多对多映射模式。 但是,也可以将联接表映射到普通实体类型,并且仅对所有操作使用两个一对多关系。

例如,这些实体类型表示两个正常表和联接表的映射,而不使用任何多对多关系:

public class Post
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class Tag
{
    public int Id { get; set; }
    public List<PostTag> PostTags { get; } = new();
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public Post Post { get; set; } = null!;
    public Tag Tag { get; set; } = null!;
}

这不需要特殊的映射,因为这些是具有正常一对多关系的正常实体类型。

21
6

Entity Framework Core:一对多关系

0
归档:2024年6月分类:C#和.NET

当单个实体与任意数量的其他实体关联时,将使用一对多关系。 例如,Blog 可以有多个关联的 Posts,但每个 Post 都只与一个 Blog 相关联。

本文档采用围绕大量示例展开的结构。 这些示例从常见情况着手,还引入了一些概念。 后面的示例介绍了不太常见的配置类型。 此处介绍了一个不错的方法,即了解前几个示例和概念,再根据特定需求转到后面的示例。 基于此方法,我们将从简单的“必需”和“可选”的一对多关系开始。

提示

可在 OneToMany.cs 中找到以下所有示例的代码。

必需的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

一对多关系由以下部分组成:

主体实体上的一个或多个主键或备用键属性,即关系的“一”端。 例如 Blog.Id。
依赖实体上的一个或多个外键属性,即关系的“多”端。 例如 Post.BlogId。
(可选)引用依赖实体的主体实体上的集合导航。 例如 Blog.Posts。
(可选)引用主体实体的依赖实体上的引用导航。 例如 Post.Blog。
因此,对于此示例中的关系:

外键属性 Post.BlogId 不可为空。 这会使关系成为“必需”关系,因为每个依赖实体 (Post) 必须与某个主体实体 (Blog) 相关,而其外键属性必须设置为某个值。
这两个实体都有指向关系另一端的相关实体的导航。
备注

必需的关系可确保每个依赖实体都必须与某个主体实体相关联。 但是,主体实体可以在没有任何依赖实体的情况下始终存在。 也就是说,必需的关系并不表示始终存在至少一个依赖实体。 无论是在 EF 模型,还是在关系数据库中,都没有确保主体实体与特定数量的依赖实体相关联的标准方法。 如果需要,则必须在应用程序(业务)逻辑中实现它。 有关详细信息,请参阅必需的导航。

提示

具有两个导航的关系(一个是从依赖实体到主体实体,一个是从主体实体到依赖实体)称为双向关系。

此关系按约定发现。 即:

Blog 作为关系中的主体实体被发现,Post 作为依赖实体被发现。
Post.BlogId 作为引用主体实体的 Blog.Id 主键的依赖实体的外键被发现。 由于 Post.BlogId 不可为空,所以发现这一关系是必需的。
Blog.Posts 作为集合导航被发现。
Post.Blog 作为引用导航被发现。
重要

使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 如果外键属性不可为空,则引用导航可以为空,也可以不为空。 在这种情况下,Post.BlogId 和 Post.Blog 皆不可为空。 = null!; 构造用于将此标记为 C# 编译器的有意行为,因为 EF 通常会对 Blog 实例进行设置,并且对于完全加载的关系,它不能为空。 有关详细信息,请参阅使用可为空引用类型。

对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

在上面的示例中,关系的配置从 主体实体类型 (Blog) 上的 HasMany 开始,然后是 WithOne。 与所有关系一样,它完全等效于从依赖实体类型开始 (Post),然后依次使用 HasOne 和 WithMany。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

与另一个选项相比,这两个选项并没有什么优势:它们都会导致完全相同的配置。

提示

没有必要对关系进行两次配置,即先从主体实体开始,又从依赖实体开始。 此外,尝试单独配置关系的主体实体和依赖实体通常不起作用。 选择从一端或另一端配置每个关系,然后只编写一次配置代码。

可选的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; } // Optional foreign key property
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

这与上一个示例相同,只不过外键属性和到主体实体的导航现在可为空。 这会使关系成为“可选”关系,因为依赖实体 (Post) 可以在无需与任何主体实体 (Blog) 相关的情况下存在。

重要

使用 C# 可为空引用类型时,如果外键属性可为空,则引用导航必须不可为空。 在本例中,Post.BlogId 不可为空,因此 Post.Blog 也不得为空。 有关详细信息,请参阅使用可为空引用类型。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired(false);
}

具有阴影外键的必需的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

在某些情况下,你可能不需要模型中的外键属性,因为外键是关系在数据库中表示方式的详细信息,而在完全以面向对象的方式使用关系时,不需要外键属性。 但是,如果要序列化实体(例如通过网络发送),则当实体不采用对象形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。

继前面的两个示例后,此示例从依赖实体类型中删除外键属性。 EF 因此会创建一个名为 BlogId 的 int 类型的阴影外键属性。

此处需要注意的一个要点是,正在使用 C# 可为空引用类型,因此引用导航的可为空性用于确定外键属性是否可为空,进而确定关系是可选的还是必需的。 如果未使用可为空引用类型,则默认情况下,阴影外键属性将为空,使关系默认为可选。 在这种情况下,使用 IsRequired 强制阴影外键属性为不可为空,并使关系成为必需关系。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired();
}

具有阴影外键的可选一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

与前面的示例一样,外键属性已从依赖实体类型中移除。 EF 因此会创建一个名为 BlogId 的 int? 类型的阴影外键属性。 与前面的示例不同,这次外键属性创建为可为空,因为正在使用 C# 可为空引用类型,并且依赖实体类型的导航可为空。 这会使关系成为可选关系。

如果未使用 C# 可为空引用类型,则默认情况下,外键属性也将创建为可为空。 这意味着,与自动创建的阴影属性的关系默认可选。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired(false);
}

无需导航到主体实体的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

在此示例中,外键属性已重新引入,但依赖实体上的导航已被移除。

提示

只有一个导航的关系(即从依赖实体到主体实体,或从主体实体到依赖实体,但只有其中一个)的被称单向关系。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

请注意,对 WithOne 的调用没有参数。 这是告知 EF 没有从 Post 到 Blog 的导航的方式。

如果从没有导航的实体开始配置,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne<Blog>()
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

无需导航到主体实体且有阴影外键的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
}

此示例通过移除外键属性和依赖实体上的导航来合并上述两个示例。

此关系按约定作为可选关系被发现。 由于代码中没有任何内容可以用来指示它是必需的,因此需要使用 IsRequired 进行最小配置来创建必需关系。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .IsRequired();
}

可以使用更完整的配置来显式配置导航和外键名称,并根据需要对 IsRequired() 或 IsRequired(false) 进行适当的调用。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey("BlogId")
        .IsRequired();
}

无需导航到依赖实体的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

前两个示例具有从主体实体到依赖实体的导航,但没有从依赖实体到主体实体的导航。 在接下来的几个示例中,将重新引入依赖实体上的导航,而主体上的导航将被移除。

如前所述,此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

请注意,WithMany() 调用时没有参数,可指示此方向没有导航。

如果从没有导航的实体开始配置,则必须使用泛型 HasMany<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

无导航的一对多
有时,配置无导航的关系可能很有用。 此类关系只能通过直接更改外键值来操作。

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

此关系不会按约定发现,因为没有任何导航指示这两种类型是相关的。 可以在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne();
}

使用此配置时,按照约定,Post.BlogId 属性仍被检测为外键,并且关系是必需的,因为外键属性不可为空。 通过将外键属性设为“可为空”,可以使关系成为“可选”关系。

此关系的更完整显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

具有备用键的一对多
在到目前为止的所有示例中,依赖实体上的外键属性被约束为主体实体上的主键属性。 外键可以改为被约束为不同的属性,该属性随后成为主体实体类型的备用键。 例如:

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系不是按约定发现的,因为 EF 始终按照约定创建与主键的关系。 可以使用对 HasPrincipalKey 的调用在 OnModelCreating 中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId);
}

HasPrincipalKey 可与其他调用结合使用,以显式配置导航、外键属性和必需/可选性质。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

具有复合外键的一对多
在到目前为止的所有示例中,主体实体的主键或备用键属性由单个属性组成。 利用多个属性也可以形成主键或备用键,这些键称为“组合键”。 当关系的主体实体具有组合键时,依赖实体的外键也必须是具有相同属性数的组合键。 例如:

// Principal (parent)
public class Blog
{
    public int Id1 { get; set; } // Composite key part 1
    public int Id2 { get; set; } // Composite key part 2
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId1 { get; set; } // Required foreign key property part 1
    public int BlogId2 { get; set; } // Required foreign key property part 2
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系按约定发现。 但是,需要显式配置组合键本身:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasKey(e => new { e.Id1, e.Id2 });
}

重要

如果组合外键值的任何属性值为空,则认为其值为 null。 具有一个属性“空”和另一个属性“非空”的组合外键不会被视为与具有相同值的主键或备用键匹配。 两者都将被视为 null。

HasForeignKey 和 HasPrincipalKey 都可用于显式指定具有多个属性的键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        nestedBuilder =>
        {
            nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

            nestedBuilder.HasMany(e => e.Posts)
                .WithOne(e => e.Blog)
                .HasPrincipalKey(e => new { e.Id1, e.Id2 })
                .HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
                .IsRequired();
        });
}

提示

在上面的代码中,对 HasKey 和 HasMany 的调用已组合到嵌套生成器中。 使用嵌套生成器,无需为同一实体类型多次调用 Entity<>(),但在功能上等效于多次调用 Entity<>()。

无需级联删除的一对多

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

按照约定,必需关系配置为级联删除,这意味着,删除主体实体后,也会删除其所有依赖实体,因为依赖实体无法存在于每月主体实体的数据库中。 可以将 EF 配置为引发异常,而不是自动删除不再存在的依赖行:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .OnDelete(DeleteBehavior.Restrict);
}

自引用一对多
在前面的所有示例中,主体实体类型与依赖实体类型有所不同。 情况不一定如此。 例如,在下面的类型中,每个 Employee 都与另一个 Employees 相关。

public class Employee
{
    public int Id { get; set; }

    public int? ManagerId { get; set; } // Optional foreign key property
    public Employee? Manager { get; set; } // Optional reference navigation to principal
    public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}

此关系按约定发现。 对于未按约定发现关系的导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .HasOne(e => e.Manager)
        .WithMany(e => e.Reports)
        .HasForeignKey(e => e.ManagerId)
        .IsRequired(false);
}

公告栏

欢迎大家来到我的博客,我是dodoro,希望我的博客能给你带来帮助。