Laravel Eloquent的自定义Query Builder:封装复杂查询逻辑与性能优化

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 语句。我们可以使用 fromRawwhereRaworderByRaw 等方法来执行原生 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 的一个强大之处在于它的链式调用。 我们可以将多个查询方法链接在一起,构建复杂的查询语句。 这种方式不仅使代码更简洁,也更易于阅读和理解。

例如,我们可以将 activewithPosts 方法链接在一起:

$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 模式的结合使用可以进一步增强代码的结构化和可测试性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注