在Laravel Eloquent中理解模型关系

模型和它们之间的关系是Laravel Eloquent的核心。如果你在使用中遇到困难,或者找不到一个简单、友好和完整的指南,就从这里开始吧!

作为一名编程文章的作者,很容易让人觉得自己非常专业和有威望。但是说实话,我在使用Eloquent时遇到了极大的困难,因为那是我第一个全栈框架。原因之一是我在工作中没有使用它,只是出于好奇而探索它;所以,我会尝试一下,然后遇到困惑,放弃,最后什么都忘记。在开始对我有意义之前,我可能已经尝试了5-6次(当然,文档没有帮助)。

但是仍然令人困惑的是Eloquent。或者至少,模型之间的关系(因为Eloquent太庞大了,无法完全学习)。对于模拟作者和博客文章的示例来说,它们只是个玩笑,因为真实的项目要复杂得多;可悲的是,官方文档使用的是同样的(或类似的)示例。即使我找到了一些有用的文章/资源,解释也很糟糕或者缺失得很严重,根本没有用。

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

最终,一点一点地,一切都开始合理起来。我终于能够正确地建模项目并舒适地使用模型了。然后有一天,我发现了一些很棒的集合技巧,让这项工作变得更加愉快。在这篇文章中,我打算涵盖所有内容,从最基础的开始,然后涵盖你在实际项目中遇到的所有可能的用例。

为什么Eloquent模型关系很难?

可悲的是,我遇到了太多不理解模型的Laravel开发者。

但是为什么呢?

即使在今天,当有许多关于Laravel的课程、文章和视频爆炸时,对它的整体理解仍然很差。我认为这是一个重要的问题,值得深思。

如果你问我,我会说Eloquent模型关系一点也不难。至少从“难”的定义来看不难。动态模式迁移是困难的;编写新的模板引擎是困难的;向Laravel的核心代码贡献代码是困难的。与这些相比,学习和使用ORM…那可不可能难!🤭🤭

实际上发生的是,学习Laravel的PHP开发者发现Eloquent很难。这才是真正的潜在问题,在我看来,有几个因素导致了这个问题(这是一个严厉的、不受欢迎的观点!):

  • 在Laravel出现之前,大多数PHP开发人员接触到的框架是CodeIgniter(顺便说一句,即使它变得更像Laravel/CakePHP,它仍然是)。在旧的CodeIgniter社区中(如果有的话),“最佳实践”是在需要的地方直接插入SQL查询。虽然我们今天有了新的CodeIgniter,但这些习惯仍然存在。因此,对于PHP开发人员来说,ORM的概念是全新的。
  • 除了少数暴露在Yii、CakePHP等框架中的PHP代码外,其他人习惯于在核心PHP或WordPress等环境中工作。这里再次强调,没有面向对象的思维方式,所以框架、服务容器、设计模式、ORM等都是陌生的概念。
  • 在PHP世界中,几乎没有持续学习的概念。普通开发人员喜欢使用单服务器设置,使用关系数据库并发出字符串形式的查询。异步编程、Web套接字、HTTP 2/3、Linux(忘记Docker)、单元测试、领域驱动设计等对于绝大多数PHP开发人员来说都是陌生的概念。因此,当遇到Eloquent时,很少有人会阅读新的、具有挑战性的东西,到达自己感觉舒适的程度。
  • 对于数据库和建模的整体理解也很差。由于数据库设计直接、紧密地与Eloquent模型相关联,这使得难度更高。

我并不是要苛刻和全球泛化 – 当然也有许多优秀的PHP开发人员,但他们的比例非常低。

如果你正在读这篇文章,那意味着你已经克服了所有这些障碍,遇到了Laravel,并且使用过Eloquent。

恭喜你!👏

你已经接近成功了。所有的基础都已经就位,我们只需要按照正确的顺序和细节进行学习。换句话说,让我们从数据库层面开始。

数据库模型:关系和基数

为了简化问题,让我们假设在本文中我们只使用。一个原因是ORM最初是为关系数据库开发的,另一个原因是RDBMS仍然非常流行。

数据模型

首先,让我们更好地了解数据模型。模型(或更准确地说,数据模型)的概念源自数据库。没有数据库,就没有数据,因此也没有数据模型。那么什么是数据模型?简单来说,它是你决定存储/组织数据的方式。例如,在电子商务店铺中,你可以将所有东西存储在一个巨大的表中(这是一个糟糕的做法,但在PHP世界中却很常见);那将是你的数据模型。你也可以将数据分成20个主表和16个连接表;这也是一种数据模型。

还要注意的是,数据库中的数据结构不一定与框架的ORM中的安排完全匹配。然而,我们总是尽力使它们尽可能接近,这样在开发时我们就不必再多考虑一件事。

基数

让我们迅速了解一下这个术语:基数。它只是指“数量”,口语上说。因此,1、2、3…都可以是某种东西的基数。故事结束。让我们继续!

关系

现在,无论我们将数据存储在任何类型的系统中,数据点之间都有相互关联的方式。我知道这听起来有些抽象和无聊,但请稍微忍耐一下。不同数据项之间的连接方式称为“关系”。让我们先看一些非数据库的例子,以确保我们完全理解这个概念。

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

所以我们可以看到,“关系”这个词在数据的上下文中没有固定的含义。事实上,如果两个人同时查看同样的数据,他们可能会识别出两个非常不同的数据关系(你好,统计学!)而且这两种关系都可以成立。

关系数据库

基于我们到目前为止讨论的所有术语,我们最终可以谈谈与Web框架(Laravel)中的模型直接相关的东西 – 关系数据库。对于我们大多数人来说,主要使用的数据库是MySQL,MariaDB,PostgreSQL,MSSQL,SQL Server,SQLite或类似的数据库。我们可能也模糊地知道这些被称为关系数据库管理系统(RDBMS),但是我们中的大多数人已经忘记了它的真正含义和重要性。

RDBMS中的“R”代表关系,当然。这不是一个任意选择的术语;通过这个,我们强调了这些数据库系统被设计成可以高效地处理存储数据之间的关系。事实上,这里的“关系”具有严格的数学意义,尽管没有开发人员需要关心它,但知道这类数据库背后存在严谨的数学是有帮助的。

浏览这些资源以获得。

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

RDBMS中的关系类型

这可能是Laravel和模型关系整个主题中最重要的部分。如果你不理解这一点,Eloquent将永远不会有意义,所以请在接下来的几分钟内注意(它甚至并不难)。

RDBMS允许我们在数据库级别上拥有数据之间的关系。这意味着这些关系不是不切实际的/虚构的/主观的,可以由不同的人通过相同的结果创建或推断出来。

与此同时,RDBMS中有某些能力/工具允许我们创建和强制这些关系,如:

  • 主键(Primary Key)
  • 外键(Foreign Key)
  • 约束(Constraints)

我不想让这篇文章变成数据库课程,所以我会假设你知道这些概念是什么。如果不知道,或者你对自己的理解感到不稳定,我推荐观看这个友好的视频(随意探索整个系列):

正如事实所证明,这些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)
);

注意这里的技巧?一般来说,在构建应用程序时这是相当罕见的,但在accounts表中,我们将字段id既设为主键又设为外键!外键属性将其与users表关联(当然 🙄),而主键属性使id列成为唯一的——真正的一对一关系!

当然,不能保证这种关系的准确性。例如,我可以添加200个新用户,却没有在accounts表中添加任何条目。如果我这样做,就会得到一个一对-关系!🤭🤭 但在纯粹的结构范围内,这是我们能做到的最好的。如果我们希望防止添加没有账户的用户,我们需要借助某种编程逻辑的帮助,可以是数据库触发器或Laravel强制执行的验证。

如果您开始感到紧张,我有一个非常好的建议:

  • 慢慢来。慢慢来,按您需要的速度来。不要试图完成本文和您今天收藏的其他15篇文章,专注于这一篇。如果需要3、4、5天,那就需要3、4、5天——您的目标应该是将Eloquent模型关系永远从您的清单中划掉。您以前跳过过很多文章,浪费了数百个小时,却没有帮助。所以,这次做些不同的事情。😇
  • 虽然这篇文章是关于Laravel Eloquent的,但所有这一切都是在很晚之后才会涉及到的。这一切的基础都是数据库模式,所以我们的重点应该是首先搞定它。如果您不能纯粹地在数据库层面上工作(假设世界上没有框架),那么模型和关系将永远不会完全理解。所以,现在忘记Laravel。完全忘记。现在我们只谈论和进行数据库设计。是的,我会时不时地提到Laravel,但您的任务是完全忽略它们,如果它们让您感到困惑的话。
  • 稍后,多了解一些关于数据库及其提供的内容。索引、性能、触发器、底层数据结构及其行为、缓存、MongoDB中的关系等等…无论您能了解到什么相关主题,都会对您作为一名工程师有帮助。要记住,框架模型只是幽灵外壳;平台的真正功能来自其底层数据库。

一对多关系

我不确定您是否意识到了这一点,但这是我们在日常工作中直观地创建的关系类型。例如,当我们创建一个orders表(一个假设的示例),以存储对users表的外键时,我们在用户和订单之间创建了一对多关系。为什么是这样呢?嗯,再从谁可以有多少的角度看一下:允许一个用户拥有多个订单,这几乎就是所有电子商务的工作原理。从相反的角度看,该关系表示一个订单只能属于一个用户,这也是非常有道理的。

在数据建模、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) 
);

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

希望现在能够理解了。或者至少,比以前更有意义了。😅请记住,就像其他任何事情一样,这只是一种实践问题,一旦你在实际情况下做了4-5次,你甚至不会再考虑它。
多对多关系
实际应用中出现的下一种关系是所谓的多对多关系。再次强调,不要担心框架甚至深入数据库之前,让我们思考一个现实中的类比:书籍和作者。想想你最喜欢的作家;他们写过不止一本书,对吧?同时,多个作者合作写一本书也很常见(至少在非小说类别中)。所以,一个作者可以写多本书,多个作者也可以写一本书。在这两个实体(书籍和作者)之间,形成了一种多对多的关系。
现在,假设你很不可能创建一个涉及图书馆、书籍和作者的真实应用程序,那么让我们考虑一些更多的例子。在B2B环境中,制造商从供应商那里订购物品,然后收到发票。发票将包含多个行项目,每个项目列出数量和供应的物品;例如,5英寸管件x 200等等。在这种情况下,物品和发票之间存在多对多的关系(自己思考并确信)。在车队管理系统中,车辆和驾驶员也会有类似的关系。在电子商务网站中,如果我们考虑收藏夹或愿望清单等功能,用户和产品之间可以存在多对多的关系。
好的,现在如何在SQL中创建这种多对多的关系?根据我们对一对多关系如何工作的了解,我们可能会认为我们应该在两个表中都存储到另一个表的外键。然而,如果我们试图这样做,会遇到很大的问题。请看下面的例子,其中书籍和作者应该有一种多对多的关系:

乍一看一切都很正常 – 书籍与作者之间的映射是一对多的关系。但是仔细看一下authors表格的数据:书籍id12和13都由Peter M.(作者id为2)编写,因此我们别无选择,只能重复条目。不仅authors表格现在存在数据完整性问题(正确的normalization之类的),id列中的值也重复了。这意味着在我们选择的设计中,不能有主键列(因为主键不能有重复值),一切都崩溃了。

显然,我们需要一种新的方法来解决这个问题,幸运的是,这个问题已经得到解决。由于直接将外键存储到两个表格中会弄乱事情,所以在关系数据库管理系统(RDBMS)中创建一对多关系的正确方法是创建一个所谓的“关联表”。基本想法是让两个原始表保持不变,并创建一个第三个表来展示一对多映射。

让我们重新做一个包含关联表的失败示例:

请注意,发生了重大变化:

  • authors表格中的列数量减少了。
  • books表格中的列数量减少了。
  • authors表格中的行数量减少了,因为不再需要重复。
  • 出现了一个名为authors_books的新表,其中包含关于哪个作者id与哪个书籍id相连的信息。我们可以将关联表格命名为任何名称,但按照惯例,结果就是简单地连接表示两个表格的下划线。

关联表格没有主键,在大多数情况下只包含两列 – 来自两个表格的ID。如果需要记录所有关系,则可以重复多少次。

现在,我们可以用肉眼看到关联表格如何清晰地显示关系,但是我们如何在应用程序中访问它们呢?秘密与名称相关 – 关联表。这不是关于SQL查询的课程,所以我不会深入研究,但是思路是如果你想要一个查询中某个特定作者的所有书籍,你可以按照相同的顺序SQL连接表格 – authorsauthors_booksbooksauthors表格和authors_books表格按照idauthor_id列进行连接,而authors_books表格和books表格按照book_idid列进行连接。

是的,这很累人。但是看到光明面吧 – 在开始研究Eloquent模型之前,我们已经完成了所有必要的理论/基础工作。让我提醒你,所有这些内容都是必需的!不了解数据库设计将导致你永远陷入Eloquent混乱之中。此外,无论Eloquent做什么或试图做什么,都完美地反映了这些数据库层面的细节,所以可以很容易地看出试图在逃避关系数据库管理系统的同时学习Eloquent是徒劳的。

在Laravel Eloquent中创建模型关系

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

假设我们要想象一个出售软玩具的商店。我们假设我们已经获得了需求文档,我们可以从中确定系统中的四个实体:用户、订单、发票、商品、类别、子类别和交易。是的,可能还有更复杂的情况,但让我们把这些放在一边,专注于如何从文档中创建一个应用程序。

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

  • 用户和订单:一对多。
  • 订单和发票:一对一。我意识到这一点并不那么明确,根据您的业务领域可能存在一对多、多对一或多对多的关系。但是当涉及到您平均的小型电子商务商店时,一个订单只会产生一个发票,反之亦然。
  • 订单和商品:多对多。
  • 商品和类别:多对一。同样,在大型电子商务网站中并非如此,但我们的操作规模很小。
  • 类别和子类别:一对多。同样,您会发现大多数现实世界的例子与此相矛盾,但是嘿,Eloquent已经足够困难了,所以让我们不要让数据建模变得更加困难!
  • 订单和交易:一对多。我还想添加这两个观点来证明我的选择:1)我们还可以在交易和发票之间建立关系。这只是一个数据建模决策。2)为什么是一对多?很常见的情况是,订单支付由于某种原因失败,下次支付成功。在这种情况下,我们为该订单创建了两个交易。我们是否希望显示这些失败的交易是一个业务决策,但是捕获有价值的数据总是一个好主意。

还有其他关系吗?还有更多的关系可能是可能的,但是它们并不实用。例如,我们可以说用户有很多交易,所以它们之间应该有关系。要在这里实现的是,已经存在一种间接关系:用户->订单->交易,一般来说,这已经足够好了,因为关系型数据库管理系统在连接表方面是非常强大的。其次,创建此关系意味着将user_id列添加到transactions表中。如果我们为每个可能的直接关系都这样做,那么我们将在数据库上增加很多负载(尤其是如果使用了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之间会有任何重大变化。有些东西已经改变并影响了本文(例如模型工厂),但我认为你能够迁移代码。

既然我们已经考虑过数据模型及其关系,那么创建模型的部分将变得微不足道。而且你会看到(我现在像一张破唱片一样重复!)它如何与数据库模式相对应,因为它完全依赖于数据库模式!

换句话说,我们需要首先为所有模型创建迁移(和模型文件),然后将其应用到数据库。稍后,我们可以在模型上工作并添加关系。

那么,我们从哪个模型开始呢?当然是最简单且最少连接的模型。在我们的情况下,这意味着User模型。由于Laravel附带了该模型(并且没有它就无法工作 🤣),让我们修改迁移文件并清理模型,以适应我们的简单需求。

这是迁移类:

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

由于我们实际上并没有构建一个项目,所以我们不需要涉及密码、是否激活等等。我们的users表只有两列:用户的id和名称。

接下来,让我们为Category创建迁移。由于Laravel允许我们一次生成模型和迁移文件,我们将利用这个便利性,尽管现在我们不会触及模型文件。

$ php artisan make:model Category -m
模型创建成功。
已创建迁移: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
模型创建成功。
已创建迁移: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的单独列,它将存储来自categories表的ID。你猜对了,这在数据库级别上创建了一对多关系。

现在轮到项目了:

$ php artisan make:model Item -m
模型创建成功。
已创建迁移: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并四舍五入。聪明吧?

无论如何,请注意一个项目与子类别之间是多对一的关系。它也与类别相关……间接地通过子类别。我们将看到所有这些技巧的实际演示,但现在,我们需要理解这些概念,并确保我们非常清楚。

接下来是Order模型及其迁移:

$ php artisan make:model Order -m
模型创建成功。
已创建迁移: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"
创建迁移: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
模型创建成功。
创建迁移: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();
        });
    }
}

是的,这张发票表确实有很多遗漏的地方;然而,我们在这里的重点是看到了 model relationships work and not to design an entire database

好的,所以,我们到了需要创建系统的最终迁移的点了(希望如此!)。现在重点是 Transaction 模型,我们之前决定它与 Order 模型相关联。顺便说一下,这里有一个练习题给你: Transaction 模型应该与 Invoice 模型关联吗?为什么和为什么不?:)

$ php artisan make:model Transaction -m
模型创建成功。
创建迁移: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
已成功删除所有表。
已成功创建迁移表。
正在迁移:2014_10_12_000000_create_users_table
已迁移: 2014_10_12_000000_create_users_table(3.45毫秒)
正在迁移:2021_01_26_093326_create_categories_table
已迁移: 2021_01_26_093326_create_categories_table(2.67毫秒)
正在迁移:2021_01_26_140845_create_sub_categories_table
已迁移: 2021_01_26_140845_create_sub_categories_table(3.83毫秒)
正在迁移:2021_01_26_141421_create_items_table
已迁移: 2021_01_26_141421_create_items_table(6.09毫秒)
正在迁移:2021_01_26_144157_create_orders_table
已迁移: 2021_01_26_144157_create_orders_table(4.60毫秒)
正在迁移:2021_01_27_093127_create_item_order_table
已迁移: 2021_01_27_093127_create_item_order_table(3.05毫秒)
正在迁移:2021_01_27_101116_create_invoices_table
已迁移: 2021_01_27_101116_create_invoices_table(3.95毫秒)
正在迁移:2021_01_31_145806_create_transactions_table
已迁移: 2021_01_31_145806_create_transactions_table(3.54毫秒)

感谢上帝!🙏🏻🙏🏻 看起来我们顺利度过了这一关。

好了,接下来我们可以开始定义模型之间的关系了!为此,我们需要回到之前创建的模型关系列表,列出模型(表)之间的直接关系。

首先,我们已经确定用户和订单之间是一对多的关系。我们可以通过打开订单迁移文件并查看字段user_id是否存在来确认这一点。这个字段是创建关系的关键,因为我们感兴趣的任何关系都需要首先得到数据库的支持;其余的(Eloquent 语法和在哪里编写哪个函数)只是纯粹的形式。

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

belongsTo(User::class);
    }
}

这个语法对你来说应该是熟悉的;我们声明了一个名为user()的函数,用于访问拥有此订单的用户(函数名可以是任何名字;它返回的内容才是重点)。再回想一下——如果没有数据库和外键,类似$this->belongsTo的语句将毫无意义。只有因为orders表上有一个外键,Laravel 才能使用那个user_id来查找具有相同id的用户并返回它。单单依靠数据库之外的东西,Laravel 无法凭空创建关系。

现在,我们也想要能够通过$user->orders来访问用户的订单。这意味着我们需要进入User模型并编写一个用于“多”部分的函数:

hasMany(Order::class);
    }
}

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

但是,等一下!这个关系是如何工作的呢?我的意思是,在数据库级别上,users表与orders表之间没有多个连接。

实际上,存在一个现有的连接,而且事实证明,它本身就足够了 – 存储在orders表中的外键引用!这意味着,当我们像$user->orders这样说的时候,Laravel会调用orders()函数,并通过查看它了解到orders表上有一个外键。然后,它会执行SELECT * FROM orders WHERE user_id = 23并将查询结果作为集合返回。当然,拥有ORM的全部目的就是忘记SQL,但我们不应该完全忘记底层基于运行SQL查询的关系型数据库管理系统。

接下来,让我们简要介绍订单和发票模型,它们之间存在一对一的关系:

belongsTo(User::class);
    }

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

还有发票模型:

belongsTo(Order::class);
    }
}

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

现在我们来看另一种关系 – 订单和商品之间的多对多关系。回想一下,我们已经创建了一个称为item_order的中间表,用于存储主键之间的映射关系。如果一切都做得正确,定义关系并与之一起工作是非常简单的。根据Laravel文档,要定义多对多关系,方法必须返回一个belongsToMany()实例。

因此,在Item模型中:

belongsToMany(Order::class);
    }
}

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

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

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

由于已经涵盖了所有三种基本类型的关系(一对一,一对多,多对多),我将不再写出其他模型的方法,因为它们会沿用相同的方式。相反,让我们为这些模型创建工厂,创建一些虚拟数据,并看看这些关系是如何运作的!

我们该如何做到这一点呢?嗯,让我们选择快速而简单的方法,将所有内容都放入默认的种子文件中。然后,当我们运行迁移时,也会运行种子文件。所以,这就是我的 DatabaseSeeder.php 文件的样子:

现在我们再次设置数据库并进行填充:

$ php artisan migrate:fresh --seed
成功删除所有表。
成功创建迁移表。
正在迁移: 2014_10_12_000000_create_users_table
已迁移:  2014_10_12_000000_create_users_table (43.81ms)
正在迁移: 2021_01_26_093326_create_categories_table
已迁移:  2021_01_26_093326_create_categories_table (2.20ms)
正在迁移: 2021_01_26_140845_create_sub_categories_table
已迁移:  2021_01_26_140845_create_sub_categories_table (4.56ms)
正在迁移: 2021_01_26_141421_create_items_table
已迁移:  2021_01_26_141421_create_items_table (5.79ms)
正在迁移: 2021_01_26_144157_create_orders_table
已迁移:  2021_01_26_144157_create_orders_table (6.40ms)
正在迁移: 2021_01_27_093127_create_item_order_table
已迁移:  2021_01_27_093127_create_item_order_table (4.66ms)
正在迁移: 2021_01_27_101116_create_invoices_table
已迁移:  2021_01_27_101116_create_invoices_table (6.70ms)
正在迁移: 2021_01_31_145806_create_transactions_table
已迁移:  2021_01_31_145806_create_transactions_table (6.09ms)
数据库填充成功完成。

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

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

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

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

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

>>> $order = Order::find(1);
[!] 将 'Order' 别名设为本 Tinker 会话的 'AppModelsOrder'。
=> 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);
[!] 将 'Invoice' 别名设为本 Tinker 会话的 'AppModelsInvoice'。
=> 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中访问一对多模型关系

我们在用户和订单之间有一个一对多的关系。现在让我们用”Tinker”来尝试一下,并看看输出结果:

>>> User::find(1)->orders;
[!] 将 'User' 别名设为本 Tinker 会话的 'AppModelsUser'。
=> 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 对象。换句话说,是一对多的关系。

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

现在,让我们来探讨一个多对多的关系。我们在物品和订单之间有这样一种关系:

>>> $item1 = Item::find(1);
[!] 将 'Item' 别名为 'AppModelsItem' 为本次 Tinker 会话。
=> 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 的一部分,反之亦然,这是我们设置的方式。我们还可以查看存储映射关系的中间表:

>>> 使用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模型需要了解的全部内容吗?

不幸的是,不是。兔子洞非常深,还有许多更具挑战性的概念,如多态关系和performance tuning等,当您成为Laravel开发人员时,您将遇到这些概念。就目前而言,本文所涵盖的内容对于大部分开发人员而言已经足够70%的时间了。在您感觉有必要提升知识之前,还有很长的路要走。

在此须知之后,我希望您能获得最重要的见解:在programming中没有黑魔法或遥不可及的东西。只是我们不理解基础和构建方式,才使我们感到困惑和沮丧。

那么…?

投资于自己!Courses、书籍、文章、其他编程社区(Python是我最推荐的)——利用您能找到的任何资源并定期消化它们,尽管缓慢。很快,您可能会搞砸整个项目的情况将大大减少。

好了,说教够了。祝您有美好的一天!🙂

类似文章