Laravel Eloquent 的自定义 Query Builder:封装复杂查询逻辑与性能优化
大家好,今天我们来深入探讨 Laravel Eloquent 的自定义 Query Builder,以及如何利用它来封装复杂的查询逻辑并进行性能优化。在实际的 Laravel 项目开发中,我们经常会遇到一些复杂的查询需求,这些查询可能涉及多个表的关联、复杂的条件判断、甚至是需要使用原生 SQL 语句才能实现的功能。直接在 Controller 或者 Model 中编写这些复杂的查询逻辑,会导致代码冗余、可读性差、维护困难,并且不利于单元测试。而自定义 Query Builder 则提供了一种优雅的解决方案,它可以将复杂的查询逻辑封装到一个独立的类中,从而提高代码的可复用性、可读性和可维护性。
1. 为什么需要自定义 Query Builder?
在深入了解如何创建和使用自定义 Query Builder 之前,我们先来分析一下为什么要使用它。
- 代码复用性: 将常用的查询逻辑封装到 Query Builder 中,可以在多个地方重复使用,避免重复编写相同的代码。
- 代码可读性: 将复杂的查询逻辑从 Controller 和 Model 中分离出来,使代码更加清晰易懂。
- 代码可维护性: 当查询逻辑需要修改时,只需要修改 Query Builder 类,而不需要修改所有使用该查询逻辑的地方。
- 单元测试: 可以对 Query Builder 类进行单元测试,确保查询逻辑的正确性。
- 性能优化: 可以通过自定义 Query Builder 来优化查询性能,例如使用
with方法预加载关联数据、使用select方法只选择需要的字段等。 - 更清晰的职责分离: 将数据访问逻辑从业务逻辑中彻底分离,符合单一职责原则。
2. 创建自定义 Query Builder
创建自定义 Query Builder 非常简单,只需要继承 IlluminateDatabaseEloquentBuilder 类即可。
首先,创建一个新的类,例如 AppQueryBuildersUserQueryBuilder:
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
// 自定义查询逻辑
}
然后,我们需要在 Model 中指定使用我们自定义的 Query Builder。可以通过重写 Model 的 newEloquentBuilder 方法来实现:
<?php
namespace AppModels;
use AppQueryBuildersUserQueryBuilder;
use IlluminateDatabaseEloquentModel;
class User extends Model
{
/**
* 创建一个新的 Eloquent 查询生成器实例。
*
* @param IlluminateDatabaseQueryBuilder $query
* @return AppQueryBuildersUserQueryBuilder
*/
public function newEloquentBuilder($query)
{
return new UserQueryBuilder($query);
}
}
现在,当我们使用 User::query() 或者 User::where() 等方法时,实际上返回的是 UserQueryBuilder 的实例,而不是默认的 IlluminateDatabaseEloquentBuilder 实例。
3. 在 Query Builder 中添加自定义查询方法
在 UserQueryBuilder 类中,我们可以添加自定义的查询方法,来实现特定的查询逻辑。例如,我们可以添加一个 active 方法,用于查询所有激活的用户:
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
/**
* 查询所有激活的用户。
*
* @return $this
*/
public function active()
{
return $this->where('is_active', true);
}
/**
* 查询指定角色ID的用户
* @param int $roleId
* @return $this
*/
public function withRole(int $roleId)
{
return $this->whereHas('roles', function ($query) use ($roleId) {
$query->where('id', $roleId);
});
}
}
现在,我们就可以在 Controller 中使用 active 方法来查询所有激活的用户了:
<?php
namespace AppHttpControllers;
use AppModelsUser;
class UserController extends Controller
{
public function index()
{
$users = User::query()->active()->get();
return view('users.index', compact('users'));
}
public function showUsersWithRole($roleId) {
$users = User::query()->withRole($roleId)->get();
return view('users.index', compact('users'));
}
}
4. 封装复杂的查询逻辑
自定义 Query Builder 的一个主要作用是封装复杂的查询逻辑。例如,我们可能需要根据用户的多个属性(如姓名、年龄、性别等)来查询用户。我们可以将这个复杂的查询逻辑封装到一个 search 方法中:
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
/**
* 根据用户的多个属性来查询用户。
*
* @param array $criteria
* @return $this
*/
public function search(array $criteria)
{
if (isset($criteria['name'])) {
$this->where('name', 'like', '%' . $criteria['name'] . '%');
}
if (isset($criteria['age'])) {
$this->where('age', $criteria['age']);
}
if (isset($criteria['gender'])) {
$this->where('gender', $criteria['gender']);
}
return $this;
}
}
现在,我们就可以在 Controller 中使用 search 方法来根据用户的多个属性来查询用户了:
<?php
namespace AppHttpControllers;
use AppModelsUser;
use IlluminateHttpRequest;
class UserController extends Controller
{
public function index(Request $request)
{
$criteria = $request->only(['name', 'age', 'gender']);
$users = User::query()->search($criteria)->get();
return view('users.index', compact('users'));
}
}
5. 性能优化
自定义 Query Builder 还可以用于优化查询性能。例如,我们可以使用 with 方法预加载关联数据,避免 N+1 查询问题。
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
/**
* 预加载关联数据。
*
* @return $this
*/
public function withPosts()
{
return $this->with('posts');
}
}
现在,我们就可以在 Controller 中使用 withPosts 方法来预加载用户的文章数据了:
<?php
namespace AppHttpControllers;
use AppModelsUser;
class UserController extends Controller
{
public function index()
{
$users = User::query()->withPosts()->get();
return view('users.index', compact('users'));
}
}
除了 with 方法,我们还可以使用 select 方法只选择需要的字段,避免查询不必要的字段,从而提高查询性能。
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
/**
* 只选择需要的字段。
*
* @return $this
*/
public function selectNameAndEmail()
{
return $this->select(['id', 'name', 'email']);
}
}
<?php
namespace AppHttpControllers;
use AppModelsUser;
class UserController extends Controller
{
public function index()
{
$users = User::query()->selectNameAndEmail()->get();
return view('users.index', compact('users'));
}
}
6. 使用原生 SQL 语句
有时候,我们需要使用原生 SQL 语句才能实现一些复杂的查询需求。自定义 Query Builder 也支持使用原生 SQL 语句。我们可以使用 fromRaw、whereRaw、orderByRaw 等方法来执行原生 SQL 语句。
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
use IlluminateSupportFacadesDB;
class UserQueryBuilder extends Builder
{
/**
* 使用原生 SQL 语句查询用户。
*
* @return $this
*/
public function withRawSQL()
{
return $this->from(DB::raw('users WHERE age > 18'));
}
public function searchByNameRaw($name)
{
return $this->whereRaw('name LIKE ?', ['%' . $name . '%']);
}
}
<?php
namespace AppHttpControllers;
use AppModelsUser;
class UserController extends Controller
{
public function index()
{
$users = User::query()->withRawSQL()->get();
return view('users.index', compact('users'));
}
public function searchByName($name)
{
$users = User::query()->searchByNameRaw($name)->get();
return view('users.index', compact('users'));
}
}
7. Query Builder 的链式调用
Query Builder 的一个强大之处在于它的链式调用。 我们可以将多个查询方法链接在一起,构建复杂的查询语句。 这种方式不仅使代码更简洁,也更易于阅读和理解。
例如,我们可以将 active 和 withPosts 方法链接在一起:
$users = User::query()->active()->withPosts()->get();
这相当于先查询所有激活的用户,然后预加载这些用户的文章数据。
8. Query Builder 与 Scope 的对比
Laravel 的 Model Scope 也可以用于封装查询逻辑。 那么,Query Builder 和 Scope 有什么区别呢?
| 特性 | Query Builder | Scope |
|---|---|---|
| 实现方式 | 继承 IlluminateDatabaseEloquentBuilder 类 |
在 Model 中定义方法 |
| 使用方式 | User::query()->method() |
User::method() |
| 灵活性 | 更灵活,可以根据需要添加任意数量的查询方法 | 相对固定,通常用于封装通用的查询逻辑 |
| 适用场景 | 封装复杂的、可变的查询逻辑 | 封装通用的、固定的查询逻辑 |
| 可扩展性 | 易于扩展,可以创建多个 Query Builder | 扩展性相对较差,通常只在 Model 中定义 Scope |
总的来说,Query Builder 更适合封装复杂的、可变的查询逻辑,而 Scope 更适合封装通用的、固定的查询逻辑。
9. 使用 Trait 组织 Query Builder 方法
当 Query Builder 类变得越来越大时,我们可以使用 Trait 来组织 Query Builder 方法,提高代码的可读性和可维护性。
例如,我们可以创建一个 Searchable Trait,用于封装搜索相关的查询方法:
<?php
namespace AppTraits;
use IlluminateDatabaseEloquentBuilder;
trait Searchable
{
/**
* 根据用户的多个属性来查询用户。
*
* @param array $criteria
* @return $this
*/
public function scopeSearch(Builder $query, array $criteria)
{
if (isset($criteria['name'])) {
$query->where('name', 'like', '%' . $criteria['name'] . '%');
}
if (isset($criteria['age'])) {
$query->where('age', $criteria['age']);
}
if (isset($criteria['gender'])) {
$query->where('gender', $criteria['gender']);
}
return $query;
}
}
然后,在 UserQueryBuilder 类中使用 Searchable Trait:
<?php
namespace AppQueryBuilders;
use AppTraitsSearchable;
use IlluminateDatabaseEloquentBuilder;
class UserQueryBuilder extends Builder
{
use Searchable;
// 其他查询方法
}
10. 结合 Repository 模式使用
自定义 Query Builder 通常会和 Repository 模式结合使用,来实现更加清晰的架构。Repository 模式用于封装数据访问逻辑,而 Query Builder 用于构建复杂的查询语句。
在这种情况下,Repository 类会依赖于 Query Builder 类,并将 Query Builder 实例传递给 Eloquent Model。
代码示例:
首先,创建一个 Repository 接口:
<?php
namespace AppRepositories;
use IlluminateDatabaseEloquentCollection;
interface UserRepositoryInterface
{
public function getAll(): Collection;
public function findById(int $id): ?object;
public function search(array $criteria): Collection;
}
然后,创建一个 Repository 实现类:
<?php
namespace AppRepositories;
use AppModelsUser;
use AppQueryBuildersUserQueryBuilder;
use IlluminateDatabaseEloquentCollection;
class UserRepository implements UserRepositoryInterface
{
public function getAll(): Collection
{
return User::query()->get();
}
public function findById(int $id): ?object
{
return User::find($id);
}
public function search(array $criteria): Collection
{
return User::query()->search($criteria)->get();
}
}
最后,在 Controller 中使用 Repository:
<?php
namespace AppHttpControllers;
use AppRepositoriesUserRepositoryInterface;
use IlluminateHttpRequest;
class UserController extends Controller
{
private $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function index(Request $request)
{
$criteria = $request->only(['name', 'age', 'gender']);
$users = $this->userRepository->search($criteria);
return view('users.index', compact('users'));
}
}
11. 实际案例:电商平台的商品搜索
假设我们正在开发一个电商平台,需要实现商品搜索功能。 用户可以根据商品名称、分类、价格范围、品牌等条件来搜索商品。
我们可以使用自定义 Query Builder 来封装这个复杂的搜索逻辑。
首先,创建一个 ProductQueryBuilder 类:
<?php
namespace AppQueryBuilders;
use IlluminateDatabaseEloquentBuilder;
class ProductQueryBuilder extends Builder
{
/**
* 根据商品名称搜索商品。
*
* @param string $name
* @return $this
*/
public function searchByName(string $name)
{
return $this->where('name', 'like', '%' . $name . '%');
}
/**
* 根据分类搜索商品。
*
* @param int $categoryId
* @return $this
*/
public function searchByCategory(int $categoryId)
{
return $this->where('category_id', $categoryId);
}
/**
* 根据价格范围搜索商品。
*
* @param float $minPrice
* @param float $maxPrice
* @return $this
*/
public function searchByPriceRange(float $minPrice, float $maxPrice)
{
return $this->whereBetween('price', [$minPrice, $maxPrice]);
}
/**
* 根据品牌搜索商品。
*
* @param int $brandId
* @return $this
*/
public function searchByBrand(int $brandId)
{
return $this->where('brand_id', $brandId);
}
/**
* 综合搜索商品。
*
* @param array $criteria
* @return $this
*/
public function search(array $criteria)
{
if (isset($criteria['name'])) {
$this->searchByName($criteria['name']);
}
if (isset($criteria['category_id'])) {
$this->searchByCategory($criteria['category_id']);
}
if (isset($criteria['min_price']) && isset($criteria['max_price'])) {
$this->searchByPriceRange($criteria['min_price'], $criteria['max_price']);
}
if (isset($criteria['brand_id'])) {
$this->searchByBrand($criteria['brand_id']);
}
return $this;
}
}
然后,在 Product Model 中指定使用 ProductQueryBuilder:
<?php
namespace AppModels;
use AppQueryBuildersProductQueryBuilder;
use IlluminateDatabaseEloquentModel;
class Product extends Model
{
/**
* 创建一个新的 Eloquent 查询生成器实例。
*
* @param IlluminateDatabaseQueryBuilder $query
* @return AppQueryBuildersProductQueryBuilder
*/
public function newEloquentBuilder($query)
{
return new ProductQueryBuilder($query);
}
}
最后,在 Controller 中使用 ProductQueryBuilder 来搜索商品:
<?php
namespace AppHttpControllers;
use AppModelsProduct;
use IlluminateHttpRequest;
class ProductController extends Controller
{
public function search(Request $request)
{
$criteria = $request->only(['name', 'category_id', 'min_price', 'max_price', 'brand_id']);
$products = Product::query()->search($criteria)->get();
return view('products.index', compact('products'));
}
}
12. 总结
自定义 Query Builder 是一种强大的工具,可以用于封装复杂的查询逻辑,提高代码的可复用性、可读性和可维护性。通过自定义 Query Builder,我们可以将数据访问逻辑从业务逻辑中彻底分离,使得代码更加清晰、易于测试和维护。同时,我们还可以利用 Query Builder 进行性能优化,例如预加载关联数据、只选择需要的字段等。
通过把复杂的查询逻辑封装到自定义的 Query Builder 中,可以有效地提升代码的质量和可维护性。它与 Repository 模式的结合使用可以进一步增强代码的结构化和可测试性。