Eloquent 查询 - 从入门到高级技巧
发布日期:作者: 史蒂夫·麦克杜格尔
准备通过本 Eloquent 查询教程提升你的 Laravel 技能!你将学到所有你需要知道的知识,从入门到高级技巧。
首先,让我们退一步,思考一下 Eloquent 是什么。Eloquent 是 Laravel 的对象关系映射器和查询构建器。你可以使用 ORM 与 Eloquent 模型一起工作,以流畅有效的方式查询你的数据库。你也可以通过数据库外观使用查询构建器来手动构建你的查询。
我们今天要查询什么?我有一个应用程序,我一直为我的 Laracon EU 演讲工作,这是一个银行应用程序 - 很激动人心,我知道。但它在查询数据时引入了一些有趣的选择。
我们正在处理的数据;用户可以拥有多个账户。每个账户都有一个存储在其上的运行余额。每个账户都有多个交易,而交易链接账户和供应商以及交易的金额。如果你想查看视觉表示,可以查看 此处的 SQL 图表。
在我的 Laracon 演讲中,我正在对查询做一些特别的事情,因为它专注于 API 本身。但是,我们可以使用许多其他查询 - 所以让我们一起来看看它们。
要获取已登录用户的全部账户,我们可以像以下这样简单地编写:
use App\Models\Account; $accounts = Account::where('user_id', auth()->id())->get();
通常,我们会在我们的控制器中编写此查询并返回结果。我不会在本教程中介绍响应机制 - 因为我不想透露太多。但是,可能有多个地方需要出于相同原因运行相同的查询。例如,如果我们正在构建一个内部仪表板并想从管理员角度查看用户的账户。
首先,我个人的困扰是不为每个查询创建新的 Eloquent Builder - 这太容易了。它允许你完全的 IDE 代码补全,而无需任何额外的文件。为此,我们将查询的第一部分设置为对 query
的静态调用。
use App\Models\Account; $accounts = Account::query()->where('user_id', auth()->id())->get();
这只是对你查询的一个简单补充,不需要花费任何时间,并且比不断转发静态调用提供更好的优势。这是你可能习惯在应用程序中看到的标准查询,有些人会说它很好 - 他们是对的。它完全满足你的需要。
我们不必使用 ORM,而是可以使用 DB 外观运行此查询 - 当然,它占用更少的内存,并且返回速度会快一些。除非你拥有大型数据集,否则你不太可能看到速度差异。但是,让我们来看一下这个查询。
use Illuminate\Support\Facades\DB; $accounts = DB::table('accounts')->where('user_id', auth()->id())->get();
在我的测试中,DB 外观使用了更少的内存,但这是因为它返回了一个对象集合。而 ORM 查询将返回一个需要构建并存储在内存中的模型集合。所以我们为拥有 Eloquent 模型的便利而付出了代价。
让我们继续前进。在我的示例中,我有一个控制器,它在行内运行此查询并返回结果。我已经提到过,此查询可以在其他应用程序区域重复使用,那么我该怎么做才能确保我可以更全局地控制这些查询?查询类来救援!
这是一个我经常使用的模式,如果你不打算采用它,你至少应该考虑一下。这是我从 CQRS 世界中学到的一个技巧,其中读取操作被归类为 查询
,而写入操作被归类为 命令
。我喜欢 CQRS 的一点是它能够将逻辑隔离在控制器需要了解的内容和专门用于查询数据的类之间。让我们来看看这个类。
final class FetchAccountsForUser implements FetchAccountsForUserContract{ public function handle(string $user): Collection { return Account::query() ->where('user_id', $user) ->get(); }}
这是一个只做一件事的单一查询类,按照典型的史蒂夫风格,它使用了一个契约/接口,这样我就可以将其移至容器并在需要的地方解析查询。因此,现在在我们的控制器中,我们只需要运行以下代码:
$accounts = $this->query->handle( user: auth()->id(),);
我们这样做有什么好处?首先,我们将逻辑隔离在一个专门的类中。如果我们获取用户账户的方式发生变化,我们可以轻松地在整个代码库中更新它。
因此,当在你的应用程序中查询数据时,你会经常发现查询并非那么动态。是的,你要传入的值将是动态的,基于用户的输入。但是,查询有时会发生变化。这并不总是如此;例如,一个 API 端点,它具有用于包含关系、过滤、排序结果等的选项。
我们在应用程序中引入了一个新问题。如何在应用程序中支持动态查询和非动态查询,而无需使用不同的工作流程?到目前为止,我们已经重构了代码,使用专门用于运行查询并返回结果的查询类。
我可以通过将查询构建器传入查询类来解决这个问题,这将使我能够将我需要的动态部分转换为更静态的部分。让我们看看如何处理这个问题。
final class FetchTransactionForAccount implements FetchTransactionForAccountContract{ public function handle(Builder $query, string $account): Builder { return $query->where('account_id', $account); }}
然后,我们将在控制器中以以下方式调用它。
public function __invoke(Request $request, string $account): JsonResponse{ $transactions = $this->query->handle( query: Transaction::query(), account: $account, )->get();}
我们可以通过在我们的控制器中传入 Transaction::query()
和账户的参考 ID 来实现这一点。查询类返回一个查询构建器实例,因此我们需要在结果上返回 get
。这个简单的示例可能无法很好地突出显示优势,因此我将介绍另一种方法。
假设我们有一个查询,我们总是希望返回一组关系并应用作用域。例如,我们想显示用户最新的账户,以及交易的总数。
$accounts = Account::query() ->withCount('transactions') ->whereHas('transactions', function (Builder $query) { $query->where( 'created_at', now()->subDays(7), ) })->latest()->get();
在行内,这是一个合理的查询。但如果我们在多个地方都有这个查询,而我们突然需要开始扩展它来添加额外的作用域,或者只显示在过去 30 天内活跃的账户……你可以想象这可能会很快变得很复杂。
让我们看看如何在查询类方法中实现它。
final class RecentAccountsForUser implements RecentAccountsForUserContract{ public function handle(Builder $query, int $days = 7): Builder { $query ->withCount('transactions') ->whereHas('transactions', function (Builder $query) { $query->where( 'created_at', now()->subDays($days), ) }); }}
当我们来实现它时
public function __invoke(Request $request): JsonResponse{ $accounts = $this->query->handle( query: Account::query()->where('user_id', auth()->id()), )->latest()->get(); // handle the return.}
更简洁,而且因为我们为查询的主体部分创建了一个专门的类 - 因此它非常可重复。
但这有必要吗?我知道很多人会简单地将它添加到模型上的特定方法中,这样就可以了。但随后,我们会随着每个请求的更改而使我们的模型变得更大,因为我们都知道,我们更倾向于添加一个辅助方法而不是替换它。使用这种方法,你会衡量添加它的好处与扩展现有内容的好处。不知不觉,你的模型上有了 30 个这样的辅助方法,这些方法必须添加到每个从集合中返回的模型中。
如果我们想在整个应用程序中使用 DB 外观怎么办?突然之间,我们有很多逻辑需要在许多地方进行更改,并且我们的结果变得非常不可预测。让我们看看使用 DB 外观的这个查询是什么样的。
$latestAccounts = DB::table( 'transactions')->join( 'accounts', 'transactions.account_id', '=', 'accounts.id')->select( 'accounts.*', DB::raw( 'COUNT(transactions.id) as total_transactions'))->groupBy( 'transactions.account_id')->orderBy('transactions.created_at', 'desc')->get();
我们将其分解成这个查询类如何呢?
final class RecentAccountsForUser implements RecentAccountsForUserContract{ public function handle(Builder $query, int $days = 7): Builder { return $query->select( 'accounts.*', DB::raw('COUNT(transactions.id) as total_transactions') )->groupBy('transactions.account_id'); }}
然后在我们的实现中,它将如下所示:
public function __invoke(Request $request): JsonResponse{ $accounts = $this->query->handle( query: DB::table( 'transactions' )->join( 'accounts', 'transactions.account_id', '=', 'accounts.id' )->where('accounts.user_id', auth()->id()), )->orderBy('transactions.created_at', 'desc')->get(); // handle the return.}
这是一个相当大的变化。但是,我们可以分阶段进行,同时测试每个小部分。这种方法的好处是,我们可以对一个用户、多个用户或所有用户使用相同的查询 - 并且查询类无需更改。
总的来说,我们所做的是创建了一些类似于作用域,但与 Eloquent Builder 本身关联较少的东西。
这就是我喜欢在我的应用程序中管理 Eloquent 查询的方式,因为它允许我拥有可重复的部分,这些部分可以独立测试各种传入选项。我认为这是一个有效的编写查询的方法,但它并不适合所有人 - 我 与 Matt Stauffer 最近的文章 证明了这一点!我刚刚做的一切都可以使用模型上的辅助方法甚至查询作用域来实现 - 但我喜欢我的模型轻便,并且我的作用域也轻便且特定。在我看来,在作用域中添加过多的逻辑感觉不对。它感觉不属于这里。当然,我可能是错的,我总是乐于接受我的方法并不是解决问题唯一的方法。
Laravel News 的技术作家,Laravel News,Treblle 的开发者倡导者。API 专家,资深 PHP/Laravel 工程师。 YouTube 直播主.