andanteproject / shared-query-builder
一个 Doctrine 2 查询构建器装饰器,使在共享上下文中构建查询更加容易
Requires
- php: ^8.0
- doctrine/orm: ^3.0
Requires (Dev)
- ext-json: *
- doctrine/doctrine-bundle: ^2.2
- friendsofphp/php-cs-fixer: ^3.58
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.11
- phpstan/phpstan-phpunit: ^1.3
- phpstan/phpstan-symfony: ^1.3
- phpunit/phpunit: ^9.5
- roave/security-advisories: dev-master
- symfony/framework-bundle: ^4.4 | ^5.0 | ^6.0 | ^7.0
README
共享查询构建器
Doctrine 2 查询构建器装饰器 - AndanteProject
一个 Doctrine 2 查询构建器装饰器,使在共享上下文中构建查询更加容易。
为什么我需要这个?
当你的查询业务逻辑很大且复杂时,你可能需要将其构建过程拆分到不同的地方/类。
没有 SharedQueryBuilder
,除非 猜测实体别名 并与 连接语句 糟糕地打交道,否则无法做到这一点。
这个 查询构建器装饰器解决了一些你在实际情况下会遇到的问题,你通常会用工作程序和业务惯例来解决这个问题。
特性
- 当你在创建上下文之外时,询问查询构建器用于实体的别名;
- 延迟连接,仅当定义了相关标准时才执行连接语句;
- 不可变和唯一的查询 参数;
- 像魔法一样 ✨。
要求
Doctrine 2 和 PHP 7.4。
安装
通过 Composer
$ composer require andanteproject/shared-query-builder
设置
在创建你的 查询构建器之后,将其包裹在我们的 SharedQueryBuilder
中。
use Andante\Doctrine\ORM\SharedQueryBuilder; // $qb instanceof Doctrine\ORM\QueryBuilder // $userRepository instanceof Doctrine\ORM\EntityRepository $qb = $userRepository->createQueryBuilder('u'); // Let's wrap query builder inside our decorator. // We use $sqb as acronym of "Shared Query Builder" $sqb = SharedQueryBuilder::wrap($qb);
从现在起,你可以像通常使用 查询构建器一样使用 $sqb
(QueryBuilder
的每个方法都在 SharedQueryBuilder
上可用),但有一些有用的额外方法 🤫。
当你完成查询构建后,只需 解包 你的 SharedQueryBuilder
。
// $sqb instanceof Andante\Doctrine\ORM\SharedQueryBuilder // $qb instanceof Doctrine\ORM\QueryBuilder $qb = $sqb->unwrap();
请注意
- 构建
SharedQueryBuilder
的唯一条件是尚未声明任何连接语句。 SharedQueryBuilder
是QueryBuilder
的 装饰器,这意味着它不是QueryBuilder
的实例,即使它有所有方法(遗憾的是,Doctrine 没有QueryBuilder 接口 🥺)。SharedQueryBuilder
不允许你使用不同的别名多次连接实体。
我有什么额外的方法?
实体方法
你可以询问 SharedQueryBuilder
是否在 from
语句或某些 join
语句中具有实体。
if($sqb->hasEntity(User::class)) // bool returned { // Apply some query criteria only if this query builder is handling the User entity }
你可以询问在构建的查询中实体的别名是什么(无论它是否用于 from
语句或 join
语句)。
$userAlias = $sqb->getAliasForEntity(User::class); // string 'u' returned
你可以使用 withAlias
方法轻松添加对该实体属性的条件的条件。
if($sqb->hasEntity(User::class)) // bool returned { $sqb ->andWhere( $sqb->expr()->eq( $sqb->withAlias(User::class, 'email'), // string 'u.email' ':email_value' ) ) ->setParameter('email_value', 'user@email.com') ; }
给定一个别名,你可以检索其实体类。
$entityClass = $sqb->getEntityForAlias('u'); // string 'App\Entity\User' returned
QueryBuilder::getAllAliases
方法扩展了一个可选的 bool
参数 $includeLazy
(默认:false
),用于包含 延迟连接 别名。
$allAliases = $sqb->getAllAliases(true);
延迟连接
所有查询构建器的 join
方法都可以像通常一样使用,但您也可以使用带 "lazy
" 前缀的方式使用它们。
// Common join methods $sqb->join(/* args */); $sqb->innerJoin(/* args */); $sqb->leftJoin(/* args */); // Lazy join methods $sqb->lazyJoin(/* args */); $sqb->lazyInnerJoin(/* args */); $sqb->lazyLeftJoin(/* args */); // They works with all the ways you know you can perform joins in Doctrine // A: $sqb->lazyJoin('u.address', 'a') // or B: $sqb->lazyJoin('Address::class', 'a', Expr\Join::WITH, $sqb->expr()->eq('u.address','a'))
通过这样做,您定义了一个 join
语句,但实际上并未将其 添加 到您的 DQL 查询中。它将在您添加 另一个条件/DQL 部分 并引用它时自动添加。神奇!✨。
根据您现在有多困惑,您可以查看 为什么您需要这个 或 一些示例 来达到您的“OMG”顿悟时刻。
示例
假设我们需要列出 User
实体,但我们还有一个可选的过滤器,可以根据 Building
名称搜索用户。
在决定使用该过滤器之前,我们无需执行任何连接。我们可以使用 延迟连接 来实现这一点。
$sqb = SharedQueryBuilder::wrap($userRepository->createQueryBuilder('u')); $sqb ->lazyJoin('u.address', 'a') ->lazyJoin('a.building', 'b') //Let's add a WHERE condition that do not need our lazy joins ->andWhere( $sqb->expr()->eq('u.verifiedEmail', ':verified_email') ) ->setParameter('verified_email', true) ; $users = $sqb->getQuery()->getResults(); // DQL executed: // SELECT u // FROM App\entity\User // WHERE u.verifiedEmail = true // BUT if we use the same Query Builder to filter by building.name: $buildingNameFilter = 'Building A'; $sqb ->andWhere( $sqb->expr()->eq('b.name', ':name_value') ) ->setParameter('name_value', $buildingNameFilter) ; $users = $sqb->getQuery()->getResults(); // DQL executed: // SELECT u // FROM App\entity\User // JOIN u.address a // JOIN a.building b // WHERE u.verifiedEmail = true // AND b.name = 'Building A'
您可能正在想:为什么我们不用以下更常见的方式达到相同的结果?(请注意,避免执行不必要的连接仍然是要求之一)
// How you could achieve this without SharedQueryBuilder $buildingNameFilter = 'Building A'; $qb = $userRepository->createQueryBuilder('u'); $qb ->andWhere( $qb->expr()->eq('u.verifiedEmail', ':verified_email') ) ->setParameter('verified_email', true); if(!empty($buildingNameFilter)){ $qb ->lazyJoin('u.address', 'a') ->lazyJoin('a.building', 'b') ->andWhere( $qb->expr()->eq('b.name', ':building_name_value') ) ->setParameter('building_name_value', $buildingNameFilter) ; } $users = $qb->getQuery()->getResults(); // Same result as example shown before // But this has some down sides further explained
如果在这个 同一上下文 中构建整个查询,上面的代码是完全可以的。
- 👍 您 清楚 整个查询构建过程;
- 👍 您 清楚 哪些实体被涉及;
- 👍 您 清楚 每个实体定义了哪些别名;
- 👍 您 清楚 定义了哪些查询参数及其用途。
但您有问题
- 👎 您将查询结构定义与可选的过滤标准混合在一起。
- 👎 代码很快就会变成一个难以阅读的混乱。
现实世界的案例
如果您的查询结构随着许多连接和过滤标准而增长,您可能需要将所有这些业务逻辑拆分到不同的类中。
例如,在一个后台管理员的用户列表中,您可能会在控制器中定义您的 主查询 来列出实体,并在某些 其他类 中处理 可选的过滤器。
// UserController.php class UserController extends Controller { public function index(Request $request, UserRepository $userRepository) : Response { $qb = $userRepository->createQueryBuilder('u'); $qb ->andWhere( $qb->expr()->eq('u.verifiedEmail', ':verified_email') ) ->setParameter('verified_email', true); // Now Apply some optional filters from Request // Let's suppose we have an "applyFilters" method which is giving QueryBuilder and Request // to and array of classes responsable to take care of filtering query results. $this->applyFilters($qb, $request); // Maybe have some pagination logic here too. Check KnpLabs/knp-components which is perfect for this. $users = $qb->getQuery()->getResults(); // Build our response with User entities list. } }
过滤器类可能看起来像这样
// BuildingNameFilter.php class BuildingNameFilter implements FilterInterface { public function filter(QueryBuilder $qb, Request $request): void { $buildingNameFilter = $request->query->get('building-name'); if(!empty($buildingNameFilter)){ $qb ->join('u.address', 'a') ->join('a.building', 'b') ->andWhere( $qb->expr()->eq('b.name', ':building_name_value') ) ->setParameter('building_name_value', $buildingNameFilter) ; } } }
我们在这里犯了几个多罪!💀 上下文已经改变。
- 👎 您 不清楚 整个查询构建过程。给定的 QueryBuilder 确实是对用户实体的查询吗?
- 👎 您 不清楚 哪些实体被涉及。哪些实体已经被连接?
- 👎 您 不清楚 为每个实体定义了哪些别名。我们怎么能按照惯例调用
u.address
呢?🤨 - 👎 您 清楚 已经定义了哪些参数(
$qb->getParameters()
),但您 不清楚 它们为什么被定义,其用途,并且您还可以通过在其他地方更改它们来 覆盖 它们,从而改变其他地方的行为; - 👎 在这个上下文中,我们的工作只是应用一些过滤器。我们 可以 通过添加一些连接语句来更改查询,但我们 应该避免 这样做。如果另一个过滤器也需要执行这些连接怎么办?灾难性的。😵
这就是为什么 SharedQueryBuilder 将在这些情况下拯救您的屁股。
让我们看看如何使用 SharedQueryBuilder
解决所有这些问题(现在您可以猜测为什么它会被命名为这样)。
使用 SharedQueryBuilder
,您可以
- 👍 定义 延迟连接,以便仅在需要时执行它们;
- 👍 定义一些 不可变 参数,以确保值不会被其他地方更改;
- 👍 您可以 检查一个实体是否在查询中,然后应用一些业务逻辑;
- 👍 您可以 询问查询构建器 用于特定实体的 别名,这样您就不需要猜测别名,或者使用常量在类之间共享别名(我知道您已经想到了这个 🧐)。
// UserController.php use Andante\Doctrine\ORM\SharedQueryBuilder; class UserController extends Controller { public function index(Request $request, UserRepository $userRepository) : Response { $sqb = SharedQueryBuilder::wrap($userRepository->createQueryBuilder('u')); $sqb // Please note: Sure, you can mix "normal" join methods and "lazy" join methods ->lazyJoin('u.address', 'a') ->lazyJoin('a.building', 'b') ->andWhere($sqb->expr()->eq('u.verifiedEmail', ':verified_email')) ->setImmutableParameter('verified_email', true); // Now Apply some optional filters from Request // Let's suppose we have an "applyFilters" method which is giving QueryBuilder and Request // to and array of classes responsable to take care of filtering query results. $this->applyFilters($sqb, $request); // Maybe have some pagination logic here too. // You probably need to unwrap the Query Builder now for this $qb = $sqb->unwrap(); $users = $qb->getQuery()->getResults(); // Build our response with User entities list. } }
过滤器类将如下所示
// BuildingNameFilter.php use Andante\Doctrine\ORM\SharedQueryBuilder; class BuildingNameFilter implements FilterInterface { public function filter(SharedQueryBuilder $sqb, Request $request): void { $buildingNameFilter = $request->query->get('building-name'); // Let's check if Query has a Building entity in from or join DQL parts 🙌 if($sqb->hasEntity(Building::class) && !empty($buildingNameFilter)){ $sqb ->andWhere( // We can ask Query builder for the "Building" alias instead of guessing it/retrieve somewhere else 💋 $sqb->expr()->eq($sqb->withAlias(Building::class, 'name'), ':building_name_value') // You can also use $sqb->getAliasForEntity(Building::class) to discover alias is 'b'; ) ->setImmutableParameter('building_name_value', $buildingNameFilter) ; } } }
- 👍 无需时不会执行额外的连接语句;
- 👍 一旦定义了参数值,就无法更改/覆盖;
- 👍 我们可以检测查询构建器是否正在处理实体,然后应用我们的业务逻辑;
- 👍 我们不会猜测实体别名;
- 👍 我们的过滤器类仅负责过滤;
- 👍 可以有多个过滤器类处理同一实体的不同条件,而不需要重复的连接语句;
不可变参数
共享查询构建器有 不可变参数。一旦定义,它们就不能更改,否则会引发 异常。
// $sqb instanceof Andante\Doctrine\ORM\SharedQueryBuilder // set a common Query Builder parameter, as you are used to $sqb->setParameter('parameter_name', 'parameterValue'); // set an immutable common Query Builder parameter. It cannot be changed otherwise an exception will be raised. $sqb->setImmutableParameter('immutable_parameter_name', 'parameterValue'); // get a collection of all query parameters (commons + immutables!) $sqb->getParameters(); // get a collection of all immutable query parameters (exclude commons) $sqb->getImmutableParameters(); // Sets a parameter and return parameter name as string instead of $sqb. $sqb->withParameter(':parameter_name', 'parameterValue'); $sqb->withImmutableParameter(':immutable_parameter_name', 'parameterValue'); // This allows you to write something like this: $sqb->expr()->eq('building.name', $sqb->withParameter(':building_name_value', $buildingNameFilter)); // The two following methods sets "unique" parameters. See "Unique parameters" doc section for more... $sqb->withUniqueParameter(':parameter_name', 'parameterValue'); $sqb->withUniqueImmutableParameter(':parameter_name', 'parameterValue');
同时设置参数并在表达式中使用它
如果您确信您不会在查询的多个位置使用参数,您可以编写以下代码 🙌
$sqb ->andWhere( $sqb->expr()->eq( $sqb->withAlias(Building::class, 'name'), ':building_name_value' ) ) ->setImmutableParameter('building_name_value', $buildingNameFilter) ;
这种方式 👇👇👇
$sqb ->andWhere( $sqb->expr()->eq( $sqb->withAlias(Building::class, 'name'), $sqb->withImmutableParameter(':building_name_value', $buildingNameFilter) // return ":building_name_value" but also sets immutable parameter ) ) ;
唯一参数
除了 不可变参数 之外,您还可以要求查询构建器生成一个参数名。使用以下方法,查询构建器将装饰名称以避免与已声明的名称冲突(即使不可变参数也不会发生这种情况)。
$sqb ->andWhere( $sqb->expr()->eq( 'building.name', $sqb->withUniqueParameter(':name', $buildingNameFilter) // return ":param_name_4b3403665fea6" making sure parameter name is not already in use and sets parameter value. ) ) ->andWhere( $sqb->expr()->gte( 'building.createdAt', $sqb->withUniqueImmutableParameter(':created_at', new \DateTime('-5 days ago')) // return ":param_created_at_5819f3ad1c0ce" making sure parameter name is not already in use and sets immutable parameter value. ) ) ->andWhere( $sqb->expr()->lte( 'building.createdAt', $sqb->withUniqueImmutableParameter(':created_at', new \DateTime('today midnight')) // return ":param_created_at_604a8362bf00c" making sure parameter name is not already in use and sets immutable parameter value. ) ) ; /* * Query Builder has now 3 parameters: * - param_name_4b3403665fea6 (common) * - param_created_at_5819f3ad1c0ce (immutable) * - param_created_at_604a8362bf00c (immutable) */
结论
世界变得更美好 💁。
如果您觉得现在世界变得更美好,请给我们一个 ⭐️!💃🏻
由 AndanteProject 团队用爱建造 ❤️。