Laravel Eloquent是当今现代框架中最强大和惊人的功能之一。从铸造数据到价值对象和类,使用可填充字段,事务,作用域,全局作用域和关系保护数据库。Eloquent使你能够在你需要使用数据库的任何地方取得成功。
开始使用Eloquent有时会让人感到害怕,因为它能做的事情太多,你永远不知道该从哪里开始。在本教程中,我将重点介绍我认为是任何应用程序的重要方面之一--向数据库写入。
你可以在任何应用程序中写入数据库:控制器、作业、中间件、artisan命令。但处理数据库写入的最佳方式是什么呢?
让我们从一个没有关系的简单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 Model上的一个静态方法。
还有一种 "老 "方法,就是建立你的模型保存到数据库中。然而,我现在很少看到它被经常使用。不过,为了清楚起见,我还是要提到它。
$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社交登录,你可以接受提供者的信息,并在验证用户之前更新或创建一个新的记录。
我们能不能再往前走一步?会有什么好处呢?你可以使用像存储库模式这样的模式,通过不同的类来 "代理 "你将发送至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 Pattern在保持一致性方面会给我们带来相当多的好处。让我们继续前进。
在某些时候,人们决定从Repository类转移到Service类是一个好主意。然而,这是同一件事 ...让我们不要去讨论这个问题。
所以,我们想要一种处理与Eloquent交互的方式,而不是那么 "内联 "或程序化的。几年前,我采用了一种方法,现在被称为 "行动"。它与Repository Pattern类似。然而,与Eloquent的每个交互都是它自己的类,而不是一个类中的方法。
让我们看看这个例子,我们有一个专门的类用于每个交互,称为 "行动"。
final class CreateNewPostAction implements CreateNewPostContract
{
public function handle(array $attributes): Model|Post
{
return Post::query()
->create(
attributes: $attributes,
);
}
}
我们的类实现了一个契约,将其与容器很好地绑定,允许我们将其注入构造函数,并在需要时用我们的数据调用处理方法。这种做法越来越流行,许多人(以及包)已经开始采用这种方法,因为你创建的实用类可以很好地做一件事--并且可以很容易地为它们创建测试替身。另一个好处是我们使用了一个接口;如果我们决定离开Eloquent(不知道你为什么要这样做),我们可以迅速改变我们的代码来反映这一点,而不需要去寻找什么。
同样,这种方法相当不错--而且原则上没有真正的坏处。我提到我是一个相当挑剔的开发者,对吗?嗯...
在使用了这么久之后,我对 "行动 "最大的问题是,我们把所有的写入、更新和删除整合都放在一个罩子里。对我来说,行动并没有把事情分割开来。如果我仔细想想,我们有两件不同的事情希望能够实现--我们希望写,我们希望读。这部分反映在另一种设计模式上,叫做CQRS(命令查询责任隔离),这是我稍稍借用的东西。在CQRS中,通常情况下,你会使用一个命令总线和一个查询总线来读写数据,通常使用事件源来发射事件进行存储。然而,有时这比你需要的工作多得多。不要误会我的意思,这种方法肯定是有时间和地点的,但你应该只在需要的时候才伸手去做--否则,你会从最小的部分过度设计你的解决方案。
因此,我把我的写动作分成 "命令",把我的读动作分成 "查询",这样我的互动就被分开了,而且很集中。让我们看一下 "命令"。
final class CreateNewPost implements CreateNewPostContract
{
public function handle(array $attributes): Model|Post
{
return Post::query()
->create(
attributes: $attributes,
);
}
}
你看一下,除了类的命名之外,它和一个动作是一样的。这就是设计。动作是向数据库写东西的一种很好的方式。我发现它们往往很快就会被挤满。
我们还有什么方法可以改进呢?引入领域转移对象将是一个好的开始,因为它提供了类型安全、上下文和一致性。
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 ,这对你的工作有帮助的话,是完全可以的。重要的是,要做你觉得舒服的事,做能使你有效的事--而不是别人说你应该在社区里做什么。
用我们刚才看的方法,我们可以用同样的方法从数据库中阅读。分解问题,确定步骤和可以改进的地方,但总是质疑你是否走得太远。如果感觉很自然,这可能是一个好的迹象。