使用 Eloquent 写入数据库
上次更新于 作者 史蒂夫·麦克杜格尔
Laravel Eloquent 是当今现代框架中最强大、最令人惊叹的功能之一。从数据转换到值对象和类,使用可填充字段、事务、作用域、全局作用域和关系保护数据库。Eloquent 使你能够在需要处理数据库的任何情况下取得成功。
开始使用 Eloquent 有时会让人感到害怕,因为它可以做很多事情,你永远不知道从哪里开始。在本教程中,我将重点关注我认为任何应用程序中必不可少的方面之一 - 写入数据库。
你可以在任何应用程序区域写入数据库:控制器、作业、中间件、Artisan 命令。那么,处理数据库写入的最佳方法是什么呢?
简单的 Eloquent 模型
让我们从一个没有关系的简单 Eloquent 模型开始。
final class Post extends Model{ protected $fillable = [ 'title', 'slug', 'content', 'published', ]; protected $casts = [ 'published' => 'boolean', ];}
我们有一个 Post
模型,它代表一个博客文章;它有一个标题、slug、内容和一个布尔标志,表示它是否已发布。在这个例子中,让我们假设已发布属性在数据库中默认为 true
。现在,首先,我们告诉 Eloquent 我们希望能够填写 title
、slug
、content
和 published
属性或列。因此,如果我们传递任何未在 fillable
数组中注册的内容,则会抛出异常 - 从潜在问题中保护我们的应用程序。
现在我们知道了哪些字段可以填写,我们可以看看如何将数据写入数据库,无论是创建、更新还是删除。如果你的模型继承了 SoftDeletes
特性,那么删除记录是一个写入操作 - 但在这个例子中,我会保持简单;删除就是删除。
你最有可能看到的,特别是在文档中,是以下内容
Post::create($request->only('title', 'slug', 'content'));
这是我所能做的标准 Eloquent,你有一个模型,你调用静态方法来创建一个新实例 - 传递一个来自请求的特定数组。这种方法有很多好处;它干净简单,每个人都能理解。我可能是一个非常固执己见的开发者。但是,我仍然会使用这种方法,尤其是在原型设计阶段,重点是测试想法而不是构建长期的东西。
我们可以通过在模型上启动一个新的 Eloquent 查询构建器实例,然后再请求创建一个新实例,从而更进一步。这将如下所示
Post::query()->create($request->only('title', 'slug', 'content'));
如你所见,它仍然非常简单,并且正成为在 Laravel 中启动查询的更标准化的方法。这种方法最大的好处之一是,query
之后的所有内容都遵循最近引入的 Query Builder Contract。由于 Laravel 的工作原理,你的 IDE 不会很好地理解静态调用 - 因为它是一个使用 __callStatic
的静态代理方法,而不是一个实际的静态方法。幸运的是,对于 query
方法来说情况并非如此,它是一个你正在扩展的 Eloquent 模型上的静态方法。
还有“较旧”的构建模型以保存到数据库的方法。但是,我很少看到它经常被使用。不过,出于清楚起见,我会提到它
$post = new Post();$post->title = $request->get('title');$post->slug = $request->get('slug');$post->content = $request->get('content');$post->save();
在这里,我们将以编程方式构建模型,将值分配给属性,然后将其保存到数据库。这样做有点繁琐,而且总是感觉为了实现目标付出了太多努力。但是,如果这是你首选的方法,那么这仍然是创建新模型的一种可接受的方法。
到目前为止,我们已经研究了三种在数据库中创建新数据的不同方法。我们可以使用类似的方法更新数据库中的数据,静态调用 update
或使用查询构建契约 query()->where('column', 'value')->update()
,最后以编程方式设置属性,然后 save
。我不会在这里重复自己,因为它与上面的内容几乎相同。
如果我们不确定记录是否已存在,该怎么办?例如,我们想要创建一个新的帖子或更新现有的帖子。我们将有一个列,我们想根据它来检查唯一性 - 然后我们传递一个我们要创建或更新的值数组,具体取决于它是否存在。
Post::query()->updateOrCreate( attributes: ['slug' => $request->get('slug'), values: [ 'title' => $request->get('title'), 'content' => $request->get('content'), ],);
如果你不确定记录是否存在,这将有一些巨大的好处,我最近在想要“确保”记录无论如何都存在时自己实现了它。例如,对于 OAuth 2.0 社交登录,你可以在身份验证用户之前接受来自提供者的信息并更新或创建新记录。
我们可以更进一步吗?好处是什么?你可以使用类似于 Repository 模式这样的模式来基本上“代理”你将发送给 Eloquent 的调用,通过一个不同的类。这样做有一些好处,或者至少在 Eloquent 成为今天的样子之前是有好处的。让我们看一个例子
class PostRepository{ private Model $model; public function __construct() { $this->model = Post::query(); } public function create(array $attributes): Model { return $this->model->create( attributes: $attributes, ); }}
如果我们使用 DB Facade 或纯 PDO,那么 Repository 模式可能会在保持一致性方面给我们带来很多好处。让我们继续。
在某个时候,人们认为从 Repository 类转移到 Service 类是一个好主意。但是,这是同样的事情……我们不要谈论这个。
因此,我们想要一种处理与 Eloquent 交互的方式,这种方式不是那么“内联”或程序化。几年前,我采用了一种现在被称为“动作”的方法。它类似于 Repository 模式。但是,每次与 Eloquent 的交互都是它自己的类,而不是一个类中的一个方法。
让我们看看这个例子,我们为每个交互都定义了一个专门的类,称为“动作”
final class CreateNewPostAction implements CreateNewPostContract{ public function handle(array $attributes): Model|Post { return Post::query() ->create( attributes: $attributes, ); }}
我们的类实现了一个契约,以将其很好地绑定到容器,使我们能够将其注入构造函数并根据需要使用我们的数据调用 handle 方法。这越来越受欢迎,许多人(以及软件包)已经开始采用这种方法,因为你创建的实用程序类只做一件事 - 并且可以轻松地为它们创建测试替身。另一个好处是,我们使用了一个接口;如果我们决定放弃 Eloquent(不知道你为什么要这样做),我们可以快速更改代码以反映这一点,而无需查找任何东西。
同样,这是一种非常好的方法 - 并且原则上没有任何真正的缺点。我说过我是一个非常挑剔的开发者,对吧?嗯……
我在使用“动作”这么长时间后遇到的最大问题是,我们将所有写入、更新和删除集成都放在一个地方。对于我来说,动作的划分还不够细致。如果我仔细想想,我们想要实现两件事 - 我们想要写入,我们想要读取。这部分反映了另一种设计模式,称为 CQRS(命令查询责任分离),这是我借鉴了一些东西。在 CQRS 中,通常你会使用命令总线和查询总线来读取和写入数据,通常会发出事件以便使用事件溯源来存储。但是,有时这比你需要的要多得多。别误会我的意思,这种方法肯定有它的时间和地点,但你应该只有在需要的时候才使用它 - 否则,你会从最小的部分过度设计你的解决方案。
因此,我将我的写入操作分为“命令”和我的读取操作分为“查询”,以便我的交互分离并专注。让我们看看一个命令
final class CreateNewPost implements CreateNewPostContract{ public function handle(array $attributes): Model|Post { return Post::query() ->create( attributes: $attributes, ); }}
看看吧,除了类名,它跟 action 一样。这是故意的。Action 是写入数据库的绝佳方式。我发现它们容易过早变得拥挤。
我们还能如何改进?引入一个 Domain Transfer Object 会是一个不错的开始,因为它提供了类型安全性、上下文和一致性。
final class CreateNewPost implements CreateNewPostContract{ public function handle(CreatePostRequest $post): Model|Post { return Post::query() ->create( attributes: $post->toArray(), ); }}
所以我们现在在一个数组中引入了类型安全性,而之前我们依赖于数组,并希望一切顺利。是的,我们可以尽情地验证——但对象具有更好的一致性。
我们还能进一步改进吗?总有改进的空间,但我们需要吗?目前的方法是可靠的、类型安全的、易于记忆的。但是,如果数据库表在我们可以写入之前被锁定,或者网络连接出现问题,比如 Cloudflare 在错误的时间宕机,我们该怎么办呢?
数据库事务将在这里拯救我们。它们的使用频率可能没有它们应该有的那么高,但它们是一个强大的工具,你应该尽快考虑采用。
final class CreateNewPost implements CreateNewPostContract{ public function handle(CreatePostRequest $post): Model|Post { return DB::transaction( fn() => Post::query()->create( attributes: $post->toArray(), ) ); }}
我们最终做到了!如果我在 PR 或代码审查中看到这样的代码,我会欣喜若狂。但是,不要觉得你必须这样写代码。记住,如果对你有用,直接内联静态 create
也是完全可以的!重要的是做你习惯的,做能让你有效率的事情——而不是做社区中其他人说你应该做的事情。
采用我们刚刚看到的方法,我们可以用相同的方式来处理从数据库中读取数据。分解问题,找出步骤以及可以改进的地方,但要始终质疑你是否做得过火了。如果感觉自然,那可能是个好兆头。
你是如何处理写入数据库的?你会走多远,什么时候是太远了?在 Twitter 上分享你的想法吧!
技术作家,在 Laravel News,开发者倡导者,在 Treblle。API 专家,资深 PHP/Laravel 工程师。YouTube 直播主。