理解 Laravel Eloquent 中的模型关系

模型和它们之间的关系是 Laravel Eloquent 的核心。 如果他们给您带来困难,或者您找不到简单、友好且完整的指南,请从这里开始!

坐在他们的编程文章的另一边,作者很容易假装或夸大平台提供的专业知识/声望的光环。 但老实说——我过得很艰难 学习 Laravel,如果仅仅是因为它是我的第一个全栈框架。 一个原因是我没有在工作中使用它,而是出于好奇探索它; 所以,我会尝试,到达一个点,感到困惑,放弃,并最终忘记一切。 在它开始对我有意义之前,我必须这样做 5-6 次(当然,文档没有帮助)。

但仍然没有意义的是 Eloquent。 或者至少,模型之间的关系(因为 Eloquent 太大而无法完全学习)。 建模作者和博客文章的示例是一个笑话,因为真实的项目要复杂得多; 遗憾的是,官方文档使用了完全相同(或相似)的示例。 或者,即使我确实遇到了一些有用的文章/资源,解释也很糟糕或严重缺失,以至于根本没有用。

(顺便说一句,我之前因为攻击官方文档而被攻击过,所以如果你有类似的想法,这是我的标准答案:去看看Django文档然后再和我谈谈。)

最终,一点一点地,它确实走到一起并变得有意义。 我终于能够正确地为项目建模并舒适地使用这些模型。 然后有一天我遇到了一些巧妙的收藏技巧,使这项工作更加愉快。 在本文中,我打算涵盖所有内容,从最基础的内容开始,然后涵盖您将在实际项目中遇到的所有可能用例。

为什么 Eloquent 模型关系很难?

可悲的是,我遇到了太多不能正确理解模型的 Laravel 开发人员。

但为什么?

即使在今天,当 Laravel 上的课程、文章和视频呈爆炸式增长时,整体理解仍然很差。 我认为这是一个重要的观点,值得反思。

如果您问我,我会说 Eloquent 模型关系一点也不难。 至少从“硬”定义的角度来看是这样。 实时模式迁移很困难; 编写一个新的模板引擎很难; 为 Laravel 的核心贡献代码是很困难的。 与这些相比,学习和使用 ORM 。 . . 嗯,这不难! 🤭🤭

实际上,学习 Laravel 的 PHP 开发人员发现 Eloquent 很难。 这是真正的根本问题,在我看来,有几个因素导致了这一点(严厉、不受欢迎的意见警报!):

  • 在 Laravel 之前,大多数 PHP 开发者接触到的一个框架是 CodeIgniter(它仍然是 ,顺便说一下,即使它变得更像 Laravel/CakePHP)。 在较旧的 CodeIgniter 社区(如果有的话)中,“最佳实践”是在需要的地方直接粘贴 SQL 查询。 尽管我们今天有一个新的 CodeIgniter,但这些习惯已经延续了下来。 因此,在学习 Laravel 时,ORM 的概念对于 PHP 开发人员来说是 100% 新鲜的。
  • 抛开极小部分 PHP 暴露于 Yii、CakePHP 等框架,其余部分习惯于在核心 PHP 或 WordPress 等环境中工作。 同样,不存在基于 OOP 的思维方式,因此不存在框架、服务容器、设计模式和 ORM。 . . 这些都是陌生的概念。
  • 在 PHP 世界中几乎没有持续学习的概念。 普通开发人员乐于使用关系数据库和发出以字符串形式编写的查询来处理单服务器设置。 异步编程、Web 套接字、HTTP 2/3、Linux(忘掉 Docker)、单元测试、领域驱动设计——对于绝大多数 PHP 开发人员来说,这些都是陌生的想法。 因此,当遇到 Eloquent 时,阅读一些新的和具有挑战性的东西,直到一个人觉得舒服的程度,是不会发生的。
  • 对数据库和建模的整体理解也很差。 由于数据库设计直接、不可分割地与 Eloquent 模型相关联,因此它提高了难度。

我并不是要在全球范围内进行苛刻和概括——也有优秀的 PHP 开发人员,而且他们中的很多人,但他们的总体百分比非常低。

如果你正在阅读这篇文章,这意味着你已经跨越了所有这些障碍,遇到了 Laravel,并且搞砸了 Eloquent。

恭喜! 👏

您快到了。 所有的构建块都已准备就绪,我们只需要按正确的顺序和细节来完成它们。 换句话说,让我们从数据库级别开始。

数据库模型:关系和基数

为简单起见,假设我们在整篇文章中只使用关系数据库。 一个原因是 ORM 最初是为关系数据库开发的; 另一个原因是 RDBMS 仍然非常流行。

数据模型

首先,让我们更好地理解数据模型。 模型(或数据模型,更准确地说)的想法来自数据库。 没有数据库,就没有数据,因此也就没有数据模型。 什么是数据模型? 很简单,这是您决定存储/构建数据的方式。 例如,在电子商务商店中,您可能将所有内容都存储在一个巨大的表中(可怕的做法,但遗憾的是,这在 PHP 世界中并不少见); 那就是你的数据模型。 您还可以将数据拆分为 20 个主表和 16 个连接表; 这也是一个数据模型。

另外,请注意,数据在数据库中的结构方式不需要 100% 匹配它在框架的 ORM 中的排列方式。 然而,努力总是让事情尽可能接近,这样我们在开发时就没有更多的事情要注意了。

基数

让我们也快速了解一下这个术语:基数。 泛指“计数”。 所以,1、2、3。 . . 都可以是某物的基数。 故事结局。 让我们继续前进!

关系

现在,无论何时我们将数据存储在任何类型的系统中,数据点都可以通过多种方式相互关联。 我知道这听起来既抽象又无聊,但请耐心等待。 不同数据项的连接方式称为关系。 让我们先看一些非数据库示例,以便我们确信我们完全理解了这个想法。

  • 如果我们将所有内容都存储在一个数组中,一种可能的关系是:下一个数据项位于比前一个索引大 1 的索引处。
  • 如果我们将数据存储在二叉树中,一种可能的关系是左侧子树的值总是小于父节点的值(如果我们选择以这种方式维护树)。
  • 如果我们将数据存储为等长数组的数组,我们可以模拟一个矩阵,然后它的属性成为我们数据的关系。

所以我们看到“关系”这个词,在数据的上下文中,没有固定的含义。 事实上,如果两个人正在查看相同的数据,他们可能会识别出两种截然不同的数据关系(你好,统计数据!)并且它们都可能是有效的。

关系数据库

基于我们迄今为止讨论的所有术语,我们终于可以谈论与 Web 框架 (Laravel) 中的模型有直接联系的东西 — 关系数据库。 对于我们大多数人来说,使用的主要数据库是 MySQL、MariaDB、PostgreSQL、MSSQL、SQL Server、SQLite 或类似的东西。 我们也可能模糊地知道这些称为 RDBMS,但我们大多数人都忘记了它的实际含义以及它为什么重要。

当然,RDBMS 中的“R”代表关系。 这不是一个任意选择的术语; 通过这一点,我们强调了一个事实,即这些数据库系统旨在有效地处理存储的数据之间的关系。 事实上,这里的“关系”具有严格的数学含义,虽然开发人员不需要为此操心,但知道有一个严格的数学 基础 在这些类型的数据库之下。

浏览这些资源以学习 SQL 和 NoSQL。

好的,根据经验我们知道 RDBMS 中的数据存储为表。 那么,关系在哪里呢?

RDBMS 中的关系类型

这可能是 Laravel 和模型关系整个主题中最重要的部分。 如果你不明白这一点,Eloquent 就永远讲不通,所以接下来的几分钟请注意(这甚至没有那么难)。

RDBMS 允许我们在数据库级别建立数据之间的关系。 这意味着这些关系不是不切实际的/想象的/主观的,并且可以由不同的人创建或推断出相同的结果。

同时,RDBMS 中有某些功能/工具允许我们创建和实施这些关系,例如:

  • 首要的关键
  • 外键
  • 约束条件

我不希望本文成为数据库课程,因此我假设您知道这些概念是什么。 如果没有,或者如果你对自己的信心感到不安,我推荐这个友好的视频(随意探索整个系列):

碰巧的是,这些 RDBMS 样式的关系也是现实世界应用程序中最常见的关系(并非总是如此,因为社交网络最好建模为图形而不是表的集合)。 因此,让我们一一了解它们,并尝试了解它们可能在哪些地方有用。

一对一关系

在几乎每个 Web 应用程序中,都有用户帐户。 此外,以下关于用户和帐户的信息(一般来说)是正确的:

  • 一个用户只能有一个帐户。
  • 一个帐户只能由一个用户拥有。

是的,我们可以争辩说,一个人可以使用另一封电子邮件注册,从而创建两个帐户,但从 Web 应用程序的角度来看,那是两个不同的人,拥有两个不同的帐户。 例如,该应用程序不会在另一个帐户中显示一个帐户的数据。

所有这些令人毛骨悚然的事情意味着——如果您的应用程序中有这样的情况并且您使用的是关系数据库,那么您需要将其设计为一对一的关系。 请注意,没有人人为地强迫你——业务领域有一个明确的情况,你恰好在使用关系数据库。 . . 只有当这两个条件都满足时,您才能建立一对一的关系。

对于这个例子(用户和账户),这是我们在创建模式时如何实现这种关系:

CREATE TABLE users(
    id INT NOT NULL AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(100) NOT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE accounts(
    id INT NOT NULL AUTO_INCREMENT,
    role VARCHAR(50) NOT NULL,
    PRIMARY KEY(id),
    FOREIGN KEY(id) REFERENCES users(id)
);

注意到这里的技巧了吗? 一般构建应用程序时很少见,但在帐户表中,我们将字段 id 设置为主键和外键! 外键属性将它链接到用户表(当然🙄),而主键属性使 id 列唯一 – 真正的一对一关系!

当然,不能保证这种关系的忠诚度。 例如,没有什么可以阻止我添加 200 个新用户而不向帐户表添加一个条目。 如果我这样做,我最终会得到一对零的关系! 🤭🤭 但在纯结构的范围内,这是我们能做的最好的。 如果我们想防止添加没有帐户的用户,我们需要从某种编程逻辑中寻求帮助,无论是数据库触发器的形式还是 Laravel 强制执行的验证。

如果您开始感到压力很大,我有一些非常好的建议:

  • 慢慢来。 尽可能慢。 与其试图完成这篇文章和您今天收藏的其他 15 篇文章,不如坚持这篇文章。 让它花 3、4、5 天,如果这是需要的话——你的目标应该是永远把 Eloquent 模型关系从你的列表中删除。 您以前从一篇文章跳到另一篇文章,浪费了数百小时,但没有任何帮助。 所以,这次做一些不同的事情。 😇
  • 虽然这篇文章是关于 Laravel Eloquent 的,但所有的内容都晚了很多。 这一切的基础是数据库模式,所以我们的重点应该放在首先把它做好。 如果您不能纯粹在数据库级别上工作(假设世界上没有框架),那么模型和关系将永远没有充分的意义。 所以,暂时忘掉 Laravel。 完全地。 我们现在只是在谈论和做数据库设计。 是的,我会时不时地引用 Laravel,但如果它们使你的情况复杂化,你的工作就是完全忽略它们。
  • 稍后,阅读更多关于数据库及其提供的内容。 MongoDB 中的索引、性能、触发器、底层数据结构及其行为、缓存、关系。 . . 您可以涵盖的任何切题都会对您作为工程师有所帮助。 请记住,框架模型只是幽灵外壳; 平台的真正功能来自其底层数据库。

一对多关系

我不确定你是否意识到这一点,但这是我们在日常工作中凭直觉建立的关系类型。 当我们创建一个订单表(一个假设的例子)时,例如,将外键存储到用户表中,我们创建了用户和订单之间的一对多关系。 这是为什么? 好吧,从谁可以拥有多少的角度再看一遍:允许一个用户拥有多个订单,这几乎是所有电子商务的运作方式。 而反过来看,这个关系是说一个订单只能属于一个用户,这也很有道理。

在数据建模、RDBMS 书籍和系统文档中,这种情况用图表表示如下:

注意到构成三叉戟的三条线了吗? 这是“很多”的符号,所以这张图表示一个用户可以有很多订单。

顺便说一句,我们反复遇到的这些“很多”和“一个”计数就是所谓的关系的基数(还记得上一节中的这个词吗?)。 同样,对于本文,该术语没有用处,但它有助于了解这个概念,以防它在采访或进一步阅读中出现。

简单吧? 而在实际 SQL 方面,创建这种关系也很简单。 事实上,它比一对一关系的情况要简单得多!

CREATE TABLE users( 
    id INT NOT NULL AUTO_INCREMENT, 
    email VARCHAR(100) NOT NULL, 
    password VARCHAR(100) NOT NULL, 
    PRIMARY KEY(id) 
);

CREATE TABLE orders( 
    id INT NOT NULL AUTO_INCREMENT, 
    user_id INT NOT NULL, 
    description VARCHAR(50) NOT NULL, 
    PRIMARY KEY(id), 
    FOREIGN KEY(user_id) REFERENCES users(id) 
);

订单表存储每个订单的用户 ID。 由于没有约束(限制)订单表中的用户 ID 必须是唯一的,这意味着我们可以多次重复单个 ID。 这就是创建一对多关系的原因,而不是隐藏在下面的一些神秘魔法。 用户 ID 在订单表中以一种愚蠢的方式存储,SQL 没有任何一对多、一对一等概念。但是一旦我们以这种方式存储数据,我们可以认为存在一对多的关系。

希望它现在有意义。 或者至少,比以前更有意义。 😀 记住,就像其他任何事情一样,这只是练习的问题,一旦你在现实世界中这样做了 4-5 次,你就不会再去想它了。

多对多关系

实践中出现的下一种关系是所谓的多对多关系。 再一次,在担心框架甚至深入研究数据库之前,让我们想想现实世界的类比:书籍和作者。 想想你最喜欢的作家; 他们写了不止一本书,对吧? 同时,看到几位作者合作写一本书(至少在非小说类型)是很常见的。 所以,一个作者可以写很多本书,许多作者可以写一本书。 在两个实体(书籍和作者)之间,这形成了多对多关系。

现在,假设您不太可能创建涉及图书馆或书籍和作者的真实应用程序,那么让我们考虑更多示例。 在 B2B 设置中,制造商从供应商处订购商品,然后收到发票。 发票将包含多个项目,每个项目都列出供应的数量和项目; 例如,5英寸管件x 200等。在这种情况下,物品和发票是多对多的关系(想想说服自己)。 在车队管理系统中,车辆和司机会有类似的关系。 在电子商务网站中,如果我们考虑收藏夹或愿望清单等功能,用户和产品可以具有多对多关系。

很公平,现在如何在 SQL 中创建这种多对多关系? 基于我们对一对多关系如何工作的了解,可能很容易认为我们应该将外键存储到两个表中的另一个表中。 但是,如果我们尝试这样做,我们会遇到重大问题。 看一下这个例子,其中书籍作者应该具有多对多关系:

乍一看,一切看起来都很好——书籍以多对多的方式映射到作者。 但仔细查看 authors 表数据:book id 12 和 13 都是由 Peter M.(作者 id 2)撰写的,因此我们别无选择,只能重复这些条目。 不仅 authors 表现在有数据完整性问题(正确 正常化 以及所有这些),id 列中的值现在重复。 这意味着在我们选择的设计中,不能有主键列(因为主键不能有重复值),一切都会分崩离析。

显然,我们需要一种新的方法来做到这一点,幸运的是,这个问题已经解决了。 由于将外键直接存储到两个表中会搞砸,在 RDBMS 中创建多对多关系的正确方法是创建一个所谓的“连接表”。 这个想法基本上是让两个原始表不受干扰并创建第三个表来演示多对多映射。

让我们重做失败的示例以包含连接表:

请注意,发生了巨大的变化:

  • 作者表中的列数减少了。
  • books 表中的列数减少了。
  • 由于不再需要重复,作者表中的行数减少了。
  • 出现了一个名为 authors_books 的新表,其中包含关于哪个作者 ID 与哪个图书 ID 相关联的信息。 我们可以将连接表命名为任何名称,但按照惯例,它只是使用下划线连接它所代表的两个表的结果。

连接表没有主键,在大多数情况下只包含两列——来自两个表的 ID。 就好像我们从前面的示例中删除了外键列并将它们粘贴到这个新表中一样。 由于没有主键,因此可以根据需要重复记录所有关系。

现在,我们可以用肉眼看到连接表是如何清楚地显示关系的,但是我们如何在我们的应用程序中访问它们呢? 秘密与名称相关联——joining table。 这不是一门关于 SQL 查询的课程,所以我不会深入研究它,但我的想法是,如果你想要一个特定作者的所有书籍在一个高效的查询中,你可以以相同的顺序 SQL-join 表 –>作者、authors_books 和书籍。 authors 和 authors_books 表分别通过 id 和 author_id 列连接,而 authors_books 和 books 表分别通过 book_id 和 id 列连接。

  如何在 Outlook 中设置自动回复

筋疲力尽,是的。 但看看好的一面——我们已经完成了在处理 Eloquent 模型之前需要做的所有必要的理论/基础工作。 让我提醒你,所有这些东西都不是可选的! 不懂数据库设计会让你永远陷入 Eloquent 的混乱境地。 此外,无论 Eloquent 做什么或试图做什么,都完美地反映了这些数据库级别的细节,因此很容易理解为什么在逃避 RDBMS 的同时尝试学习 Eloquent 是徒劳的。

在 Laravel Eloquent 中创建模型关系

最后,在绕了大约 70,000 英里的弯路之后,我们已经到了可以讨论 Eloquent、它的模型以及如何创建/使用它们的地步。 现在,我们在文章的前一部分了解到,一切都始于数据库以及如何对数据建模。 这让我意识到我应该使用一个完整的例子来开始一个新的项目。 同时,我希望这个例子是真实世界的,而不是关于博客和作者或书籍和书架(它们也是真实世界,但已经死了)。

让我们想象一家销售毛绒玩具的商店。 我们还假设我们已经获得了需求文档,我们可以从中识别系统中的这四个实体:用户、订单、发票、项目、类别、子类别和交易。 是的,可能会涉及更多的复杂问题,但让我们把它放在一边,关注我们如何从文档到应用程序。

一旦确定了系统中的主要实体,我们就需要根据我们目前讨论的数据库关系来考虑它们之间的关系。 以下是我能想到的:

  • 用户和订单:一对多。
  • 订单和发票:一对一。 我意识到这不是一成不变的,根据您的业务领域,可能存在一对多、多对一或多对多关系。 但是对于普通的小型电子商务商店,一个订单只会产生一张发票,反之亦然。
  • 订单和物品:多对多。
  • 项目和类别:多对一。 同样,大型电子商务网站并非如此,但我们的业务规模很小。
  • 类别和子类别:一对多。 同样,你会发现大多数真实世界的例子都与此相矛盾,但是,嘿,Eloquent 本身就够难了,所以我们不要让数据建模更难!
  • 订单和交易:一对多。 我还想添加这两点作为我选择的理由:1) 我们也可以在 Transactions 和 Invoices 之间添加关系。 这只是一个数据建模决策。 2)为什么一对多在这里? 好吧,订单付款由于某种原因失败而下一次成功是很常见的。 在这种情况下,我们为该订单创建了两个交易。 我们是否希望显示那些失败的交易是一个业务决策,但捕获有价值的数据始终是一个好主意。

还有其他关系吗? 好吧,还有更多的关系是可能的,但它们不切实际。 比如我们可以说一个用户有很多笔交易,那么他们之间应该是有关系的。 这里要意识到的是,已经存在间接关系:用户 -> 订单 -> 交易,一般来说,这已经足够好了,因为 RDBMS 是连接表的野兽。 其次,创建这种关系意味着向交易表中添加一个 user_id 列。 如果我们对每一个可能的直接关系都这样做,那么我们就会在数据库上增加更多的负载(以更多存储的形式,尤其是在使用 UUID 和维护索引的情况下),从而链接整个系统。 当然,如果企业表示他们需要交易数据并在 1.5 秒内需要它,我们可能会决定添加这种关系并加快速度(权衡,权衡……)。

现在,女士们先生们,是时候编写实际的代码了!

Laravel 模型关系——带有代码的真实示例

本文的下一阶段是关于亲自动手——但是以一种有用的方式。 我们将选择与早期电子商务示例中相同的数据库实体,我们将直接从安装 Laravel 开始,了解 Laravel 中的模型是如何创建和连接的!

当然,我假设您已经设置了开发环境并且知道如何安装和使用 Composer 来管理依赖项。

$ composer global require laravel/installer -W
$ laravel new model-relationships-study

这两个控制台命令安装 Laravel 安装程序(-W 部分用于升级,因为我已经安装了旧版本)。 如果你很好奇,在撰写本文时,安装的 Laravel 版本是 8.5.9。 你应该惊慌并升级吗? 我不建议这样做,因为我不希望在我们的应用程序上下文中 Laravel 5 和 Laravel 8 之间有任何重大变化。 有些事情已经改变并将影响本文(例如模型工厂),但我认为您将能够移植代码。

由于我们已经仔细考虑了数据模型及其关系,因此创建模型的部分将变得微不足道。 您还会看到(我现在听起来像是一张破唱片!)它是如何反映数据库模式的,因为它 100% 依赖于它!

换句话说,我们需要首先为所有将应用于数据库的模型创建迁移(和模型文件)。 稍后,我们可以处理模型并处理关系。

那么,我们从哪个模型开始呢? 当然是最简单也是连接最少的一个。 在我们的例子中,这意味着用户模型。 由于 Laravel 附带了这个模型(没有它就无法工作 🤣),让我们修改迁移文件并清理模型以满足我们的简单需求。

这是迁移类:

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
        });
    }
}

因为我们实际上并不是在构建一个项目,所以我们不需要输入密码、is_active 等等。 我们的用户表将只有两列:用户的 ID 和名称。

接下来让我们为类别创建迁移。 由于 Laravel 允许我们在单个命令中也能方便地生成模型,我们将利用它,尽管我们现在不会触及模型文件。

$ php artisan make:model Category -m
Model created successfully.
Created Migration: 2021_01_26_093326_create_categories_table

这是迁移类:

class CreateCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
        });
    }
}

如果您对缺少 down() 函数感到惊讶,请不要这样; 实际上,您很少最终会使用它,因为删除列或表或更改列类型会导致无法恢复的数据丢失。 在开发中,您会发现自己删除了整个数据库,然后重新运行迁移。 但是我们离题了,所以让我们回过头来处理下一个实体。 由于子类别与类别直接相关,因此我认为下一步这样做是个好主意。

$ php artisan make:model SubCategory -m
Model created successfully.
Created Migration: 2021_01_26_140845_create_sub_categories_table

好的,现在让我们填写迁移文件:

class CreateSubCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('sub_categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');

            $table->unsignedBigInteger('category_id');
            $table->foreign('category_id')
                ->references('id')
                ->on('categories')
                ->onDelete('cascade');
        });
    }
}

如您所见,我们在这里添加了一个单独的列,称为 category_id,它将存储类别表中的 ID。 无需猜测,这会在数据库级别创建一对多关系。

现在轮到物品了:

$ php artisan make:model Item -m
Model created successfully.
Created Migration: 2021_01_26_141421_create_items_table

和迁移:

class CreateItemsTable extends Migration
{
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description');
            $table->string('type');
            $table->unsignedInteger('price');
            $table->unsignedInteger('quantity_in_stock');

            $table->unsignedBigInteger('sub_category_id');
            $table->foreign('sub_category_id')
                ->references('id')
                ->on('sub_categories')
                ->onDelete('cascade');
        });
    }
}

如果你觉得事情应该以不同的方式来做,那很好。 两个人很少会想出完全相同的模式和架构。 请注意一件事,这是一种最佳做法:我将价格存储为整数。

为什么?

好吧,人们意识到在数据库端处理浮动除法和所有操作都很丑陋且容易出错,因此他们开始以最小货币单位存储价格。 例如,如果我们以美元操作,这里的价格字段将代表美分。 在整个系统中,值和计算将以美分为单位; 只有在需要向用户显示或通过电子邮件发送 PDF 时,我们才会除以 100 并四舍五入。 聪明,嗯?

无论如何,请注意一个项目以多对一的关系链接到一个子类别。 它还链接到一个类别。 . . 间接通过其子类别。 我们将看到所有这些体操的实实在在的演示,但现在,我们需要理解这些概念并确保我们 100% 清楚。

接下来是订单模型及其迁移:

$ php artisan make:model Order -m
Model created successfully.
Created Migration: 2021_01_26_144157_create_orders_table

为了简洁起见,我将只包括迁移中的一些重要字段。 我的意思是,订单的详细信息可以包含很多内容,但我们会将它们限制为少数,以便我们可以专注于模型关系的概念。

class CreateOrdersTable extends Migration
{
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('status');
            $table->unsignedInteger('total_value');
            $table->unsignedInteger('taxes');
            $table->unsignedInteger('shipping_charges');

            $table->unsignedBigInteger('user_id');
            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade');
        });
    }
}

看起来不错,但是,等一下! 此订单中的项目在哪里? 正如我们之前建立的那样,订单和商品之间存在多对多关系,因此简单的外键不起作用。 解决方案是所谓的连接表或中间表。 换句话说,我们需要一个连接表来存储订单和商品之间的多对多映射。 现在,在 Laravel 世界中,有一个我们遵循的内置约定来节省时间:如果我使用两个表名的单数形式创建一个新表,将它们按字典顺序放置,并使用下划线连接它们, Laravel 会自动将其识别为连接表。

在我们的例子中,连接表将被称为 item_order(字典中“item”一词出现在“order”之前)。 此外,如前所述,此连接表通常仅包含两列,即每个表的外键。

我们可以在这里创建一个模型 + 迁移,但该模型永远不会被使用,因为它更像是一个元数据。 因此,我们在 Laravel 中创建了一个新的迁移并告诉它什么是什么。

$ php artisan make:migration create_item_order_table --create="item_order"
Created Migration: 2021_01_27_093127_create_item_order_table

这会产生一个新的迁移,我们将对其进行如下更改:

class CreateItemOrderTable extends Migration
{
    public function up()
    {
        Schema::create('item_order', function (Blueprint $table) {
            $table->unsignedBigInteger('order_id');
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade');
            
            $table->unsignedBigInteger('item_id');
            $table->foreign('item_id')
                ->references('id')
                ->on('items')
                ->onDelete('cascade');    
        });
    }
}

如何通过 Eloquent 方法调用实际访问这些关系是稍后的主题,但请注意,我们需要首先煞费苦心地手动创建这些外键。 没有这些,就没有 Eloquent,Laravel 就没有“智能”。 🙂

我们到了吗? 嗯,差不多。 . .

我们只需要担心几个模型。 第一个是发票表,您会记得我们决定使它与订单成为一对一的关系。

$ php artisan make:model Invoice -m
Model created successfully.
Created Migration: 2021_01_27_101116_create_invoices_table

在本文最前面的部分中,我们看到强制一对一关系的一种方法是使子表上的主键也成为外键。 在实践中,几乎没有人采用这种过于谨慎的方法,人们通常按照一对多关系来设计模式。 我的看法是中间方法更好; 只需使外键唯一,并确保父模型的 ID 不能重复:

class CreateInvoicesTable extends Migration
{
    public function up()
    {
        Schema::create('invoices', function (Blueprint $table) {
            $table->id();
            $table->timestamp('raised_at')->nullable();
            $table->string('status');
            $table->unsignedInteger('totalAmount');

            $table->unsignedBigInteger('order_id')->unique();
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade')
                ->unique();
        });
    }
}

是的,我已经无数次意识到这张发票表缺失了很多东西; 然而,我们这里的重点是了解模型关系是如何工作的,而不是设计整个数据库。

好的,我们已经到了需要创建系统最终迁移的地步(我希望如此!)。 现在的重点是交易模型,我们之前决定将其链接到订单模型。 顺便说一句,这里有一个练习:是否应该将交易模型链接到发票模型? 为什么,为什么不呢? 🙂

$ php artisan make:model Transaction -m
Model created successfully.
Created Migration: 2021_01_31_145806_create_transactions_table

和迁移:

class CreateTransactionsTable extends Migration
{
    public function up()
    {
        Schema::create('transactions', function (Blueprint $table) {
            $table->id();
            $table->timestamp('executed_at');
            $table->string('status');
            $table->string('payment_mode');
            $table->string('transaction_reference')->nullable();

            $table->unsignedBigInteger('order_id');
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade');
        });
    }
}

呸! 那是一些艰苦的工作。 . . 让我们运行迁移,看看我们在数据库中的表现如何。

$ php artisan migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (3.45ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated:  2021_01_26_093326_create_categories_table (2.67ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated:  2021_01_26_140845_create_sub_categories_table (3.83ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated:  2021_01_26_141421_create_items_table (6.09ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated:  2021_01_26_144157_create_orders_table (4.60ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated:  2021_01_27_093127_create_item_order_table (3.05ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated:  2021_01_27_101116_create_invoices_table (3.95ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated:  2021_01_31_145806_create_transactions_table (3.54ms)

赞美归于主! 🙏🏻🙏🏻 看来我们挺过了考验的那一刻。

这样,我们就可以继续定义模型关系了! 为此,我们需要回到我们之前创建的列表,概述模型(表)之间的直接关系类型。

首先,我们已经确定用户和订单之间存在一对多关系。 我们可以通过转到订单的迁移文件并查看那里是否存在字段 user_id 来确认这一点。 这个字段就是创建关系的原因,因为我们有兴趣建立的任何关系都需要首先被数据库接受; 其余的(雄辩的语法和在哪里写哪个函数)只是纯粹的形式。

换句话说,关系已经存在。 我们只需要告诉 Eloquent 使其在运行时可用。 让我们从 Order 模型开始,我们声明它属于 User 模型:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    use HasFactory;

    public function user() {
        return $this->belongsTo(User::class);
    }
}

您必须熟悉语法; 我们声明一个名为 user() 的函数,它用于访问拥有此订单的用户(函数名称可以是任何名称;重要的是它返回的内容)。 再回想一下——如果没有数据库也没有外键,像 $this->belongsTo 这样的语句将毫无意义。 只是因为订单表上有一个外键,Laravel 才能使用该 user_id 来查找具有相同 ID 的用户并返回它。 就其本身而言,没有数据库的配合,Laravel 无法凭空创建关系。

  与不可替代代币 (NFT) 相关的挑战和风险

现在,能够编写 $user->orders 来访问用户的订单也很好。 这意味着我们需要转到用户模型并为这种一对多关系的“多”部分编写一个函数:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class User extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function orders() {
        return $this->hasMany(Order::class);
    }
}

是的,我大量修改了默认用户模型,因为我们不需要本教程的所有其他功能。 无论如何,User 类现在有一个名为 orders() 的方法,它表示一个用户可以与多个订单相关联。 在 ORM 世界中,我们说这里的 orders() 关系与我们在 Order 模型上的 user() 关系相反。

但是,等一下! 这种关系如何运作? 我的意思是,在数据库级别没有任何东西具有从用户表到订单表的多个连接。

实际上,存在一个现有连接,事实证明,它本身就足够了 — 存储在订单表中的外键引用! 也就是说,当我们说类似 $user->orders 的东西时,Laravel 会调用 orders() 函数并通过查看它知道 orders 表上有一个外键。 然后,它执行 SELECT * FROM 命令 WHERE user_id = 23 并将查询结果作为集合返回。 当然,拥有 ORM 的全部意义在于忘记 SQL,但我们不应该完全忘记底层基础是运行 SQL 查询的 RDBMS。

接下来,让我们轻松浏览一下订单和发票模型,我们在其中具有一对一的关系:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function user() {
        return $this->belongsTo(User::class);
    }

    public function invoice() {
        return $this->hasOne(Invoice::class);
    }
}

和发票模型:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Invoice extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function order() {
        return $this->belongsTo(Order::class);
    }
}

请注意,在数据库级别,以及几乎在 Eloquent 级别,这是一个典型的一对多关系; 我们刚刚添加了一些检查以确保它保持一对一。

我们现在来看另一种关系——订单和商品之间的多对多关系。 回想一下,我们已经创建了一个名为 item_order 的中间表,用于存储主键之间的映射。 如果这些都做对了,那么定义关系并使用它就很简单了。 根据 Laravel 文档,要定义多对多关系,您的方法必须返回 belongsToMany() 实例。

因此,在 Item 模型中:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Item extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function orders() {
        return $this->belongsToMany(Order::class);
    }
}

令人惊讶的是,反向关系几乎是相同的:

class Order extends Model
{
    /* ... other code */
    
    public function items() {
        return $this->belongsToMany(Item::class);
    }
}

就是这样! 只要我们正确地遵循命名约定,Laravel 就能够推断出映射及其位置。

由于已经涵盖了所有三种基本类型的关系(一对一、一对多、多对多),我将停止编写其他模型的方法,因为它们将沿着相同的线路。 相反,让我们为这些模型创建工厂,创建一些虚拟数据,并在行动中查看这些关系!

我们该怎么做? 好吧,让我们走捷径,将所有内容都放入默认的种子文件中。 然后,当我们运行迁移时,我们也会运行播种机。 所以,这就是我的 DatabaseSeeder.php 文件的样子:

<?php

namespace DatabaseSeeders;

use IlluminateDatabaseSeeder;
use AppModelsCategory;
use AppModelsSubCategory;
use AppModelsItem;
use AppModelsOrder;
use AppModelsInvoice;
use AppModelsUser;
use Faker;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $faker = FakerFactory::create();

        // Let's make two users
        $user1 = User::create(['name' => $faker->name]);
        $user2 = User::create(['name' => $faker->name]);

        // Create two categories, each having two subcategories
        $category1 = Category::create(['name' => $faker->word]);
        $category2 = Category::create(['name' => $faker->word]);

        $subCategory1 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);
        $subCategory2 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);

        $subCategory3 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);
        $subCategory4 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);

        // After categories, well, we have items
        // Let's create two items each for sub-category 2 and 4
        $item1 = Item::create([
            'sub_category_id' => 2,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(2),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item2 = Item::create([
            'sub_category_id' => 2,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(3),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item3 = Item::create([
            'sub_category_id' => 4,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(4),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item4 = Item::create([
            'sub_category_id' => 4,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(1),
            'quantity_in_stock' => $faker->randomNumber(3),
        ]);

        // Now that we have users and items, let's make user1 place a couple of orders
        $order1 = Order::create([
            'status' => 'confirmed',
            'total_value' => $faker->randomNumber(3),
            'taxes' => $faker->randomNumber(1),
            'shipping_charges' => $faker->randomNumber(2),
            'user_id' => $user1->id
        ]);

        $order2 = Order::create([
            'status' => 'waiting',
            'total_value' => $faker->randomNumber(3),
            'taxes' => $faker->randomNumber(1),
            'shipping_charges' => $faker->randomNumber(2),
            'user_id' => $user1->id
        ]);

        // now, assigning items to orders
        $order1->items()->attach($item1);
        $order1->items()->attach($item2);
        $order1->items()->attach($item3);
        
        $order2->items()->attach($item1);
        $order2->items()->attach($item4);

        // and finally, create invoices
        $invoice1 = Invoice::create([
            'raised_at' => $faker->dateTimeThisMonth(),
            'status' => 'settled',
            'totalAmount' => $faker->randomNumber(3),
            'order_id' => $order1->id,
        ]);
    }
}

现在我们再次设置数据库并为它播种:

$ php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (43.81ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated:  2021_01_26_093326_create_categories_table (2.20ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated:  2021_01_26_140845_create_sub_categories_table (4.56ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated:  2021_01_26_141421_create_items_table (5.79ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated:  2021_01_26_144157_create_orders_table (6.40ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated:  2021_01_27_093127_create_item_order_table (4.66ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated:  2021_01_27_101116_create_invoices_table (6.70ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated:  2021_01_31_145806_create_transactions_table (6.09ms)
Database seeding completed successfully.

好的! 现在是本文的最后一部分,我们在这里简单地访问这些关系并确认我们到目前为止所学的所有内容。 您会很高兴知道(我希望)这将是一个轻量级且有趣的部分。

现在,让我们启动最有趣的 Laravel 组件——Tinker 交互式控制台!

$ php artisan tinker
Psy Shell v0.10.6 (PHP 8.0.0 — cli) by Justin Hileman
>>>

在 Laravel Eloquent 中访问一对一模型关系

好的,首先,让我们访问订单和发票模型中的一对一关系:

>>> $order = Order::find(1);
[!] Aliasing 'Order' to 'AppModelsOrder' for this Tinker session.
=> AppModelsOrder {#4108
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order->invoice
=> AppModelsInvoice {#4004
     id: 1,
     raised_at: "2021-01-21 19:20:31",
     status: "settled",
     totalAmount: 314,
     order_id: 1,
   }

注意到什么了吗? 请记住,它是在数据库级别完成的,如果没有额外的约束,这种关系是一对多的。 因此,Laravel 可以返回一组对象(或仅一个对象)作为结果,这在技术上是准确的。 但 。 . . 我们已经告诉 Laravel 这是一对一的关系,所以结果是一个 Eloquent 实例。 请注意在访问反向关系时同样的事情是如何发生的:

$invoice = Invoice::find(1);
[!] Aliasing 'Invoice' to 'AppModelsInvoice' for this Tinker session.
=> AppModelsInvoice {#3319
     id: 1,
     raised_at: "2021-01-21 19:20:31",
     status: "settled",
     totalAmount: 314,
     order_id: 1,
   }
>>> $invoice->order
=> AppModelsOrder {#4042
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }

在 Laravel Eloquent 中访问一对多模型关系

我们在用户和订单之间有一个一对多的关系。 现在让我们“修补”它并查看输出:

>>> User::find(1)->orders;
[!] Aliasing 'User' to 'AppModelsUser' for this Tinker session.
=> IlluminateDatabaseEloquentCollection {#4291
     all: [
       AppModelsOrder {#4284
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
       },
       AppModelsOrder {#4280
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
       },
     ],
   }
>>> Order::find(1)->user
=> AppModelsUser {#4281
     id: 1,
     name: "Dallas Kshlerin",
   }

正如预期的那样,访问用户的订单会产生一组记录,而相反的情况只会产生一个 User 对象。 换句话说,一对多。

  如何更新 Boost 移动塔

在 Laravel Eloquent 中访问多对多模型关系

现在,让我们探索多对多关系。 我们在商品和订单之间有这样一种关系:

>>> $item1 = Item::find(1);
[!] Aliasing 'Item' to 'AppModelsItem' for this Tinker session.
=> AppModelsItem {#4253
     id: 1,
     name: "Russ Kutch",
     description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
     type: "adipisci",
     price: 26,
     quantity_in_stock: 65,
     sub_category_id: 2,
   }
>>> $order1 = Order::find(1);
=> AppModelsOrder {#4198
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order1->items
=> IlluminateDatabaseEloquentCollection {#4255
     all: [
       AppModelsItem {#3636
         id: 1,
         name: "Russ Kutch",
         description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
         type: "adipisci",
         price: 26,
         quantity_in_stock: 65,
         sub_category_id: 2,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4264
           order_id: 1,
           item_id: 1,
         },
       },
       AppModelsItem {#3313
         id: 2,
         name: "Mr. Green Cole",
         description: "Maxime beatae porro commodi fugit hic. Et excepturi natus distinctio qui sit qui. Est non non aut necessitatibus aspernatur et aspernatur et. Voluptatem possimus consequatur exercitationem et.",
         type: "pariatur",
         price: 381,
         quantity_in_stock: 82,
         sub_category_id: 2,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4260
           order_id: 1,
           item_id: 2,
         },
       },
       AppModelsItem {#4265
         id: 3,
         name: "Brianne Weissnat IV",
         description: "Delectus ducimus quia voluptas fuga sed eos esse. Rerum repudiandae incidunt laboriosam. Ea eius omnis autem. Cum pariatur aut voluptas sint aliquam.",
         type: "non",
         price: 3843,
         quantity_in_stock: 26,
         sub_category_id: 4,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4261
           order_id: 1,
           item_id: 3,
         },
       },
     ],
   }
>>> $item1->orders
=> IlluminateDatabaseEloquentCollection {#4197
     all: [
       AppModelsOrder {#4272
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4043
           item_id: 1,
           order_id: 1,
         },
       },
       AppModelsOrder {#4274
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4257
           item_id: 1,
           order_id: 2,
         },
       },
     ],
   }

这个输出读起来可能有点令人眼花缭乱,但请注意 item1 是 order1 的项目的一部分,反之亦然,这就是我们的设置方式。 我们还可以查看存储映射的中间表:

>>> use DB;
>>> DB::table('item_order')->select('*')->get();
=> IlluminateSupportCollection {#4290
     all: [
       {#4270
         +"order_id": 1,
         +"item_id": 1,
       },
       {#4276
         +"order_id": 1,
         +"item_id": 2,
       },
       {#4268
         +"order_id": 1,
         +"item_id": 3,
       },
       {#4254
         +"order_id": 2,
         +"item_id": 1,
       },
       {#4267
         +"order_id": 2,
         +"item_id": 4,
       },
     ],
   }

结论

是的,就是这样,真的! 这是一篇很长的文章,但我希望它有用。 这是所有关于 Laravel 模型的知识吗?

可悲的是没有。 兔子洞真的非常非常深,还有许多更具挑战性的概念,例如多态关系和性能调优等等,随着您作为 Laravel 开发人员的成长,您会遇到这些概念。 目前,粗略地说,本文涵盖的内容足以满足 70% 的开发人员 70% 的时间。 过不了多久你就会觉得有必要升级你的知识。

抛开这个警告,我希望你带走这个最重要的见解:在编程中没有什么是黑魔法或遥不可及的。 只是我们不了解基础,不了解事物是如何构建的,这使我们感到挣扎和沮丧。

所以 。 . . ?

投资自己! 课程、书籍、文章、其他编程社区(Python 是我的第一推荐)——使用你能找到的任何资源,如果缓慢的话,请定期使用它们。 很快,您可能搞砸整个事情的情况就会大大减少。

好吧,足够的说教。 祝你今天过得愉快! 🙂