返回

Extbase自定义 WHERE 查询:解决复杂 SQL 条件

mysql

Extbase 自定义 WHERE 查询条件

在 TYPO3 Extbase 开发中,有时我们需要在查询中添加无法直接使用 Query 对象构建的自定义 WHERE 条件。特别是在进行地理位置相关的搜索,例如基于经纬度的半径查询时,这个问题变得尤为突出。常规的 Query 对象主要处理简单的等于、大于、小于等条件,对于复杂 SQL 片段的支持相对有限。 本文将探讨几种方法来解决这类问题,并在 Extbase 的数据检索过程中加入定制的 SQL WHERE 部分。

问题剖析

Extbase 提供了一套优雅的数据访问层,避免了开发者直接编写 SQL 语句。然而,当需要执行超出其能力范围的 SQL 时,便需要另寻出路。诸如地理半径查询这类涉及数学函数(acos, sin, cos, radians)的查询,是无法通过标准的 Query 对象的条件构建方法实现的。虽然可以使用 $query->equals()$query->greaterThan() 等方式进行普通查询,但对于这种类型的场景就无能为力了。 这会导致某些人倾向于使用原始数据库访问(例如 $GLOBALS['TYPO3_DB']->exec_SELECTquery() ),但这破坏了 Extbase 的数据抽象。更好的方式是探索一种将自定义 SQL 附加到 Extbase 查询中的方法,从而达到期望的查询效果。

解决方案

这里介绍两种有效的办法,来实现在 Extbase 查询中加入自定义 SQL WHERE 部分。

1. 使用 statement 修改 SQL 查询

Extbase 的 Query 对象提供了 statement 属性,它是生成 SQL 语句的抽象。我们可以借助这个属性在查询被执行之前直接修改 SQL 的 WHERE 部分。虽然 QueryInterface 并未提供 setStatement() 方法,但是你可以借助其他方式做到。

以下示例演示了具体的操作步骤:

  1. 获取 Querystatement 属性: Query 对象有一个受保护的属性,但可以通过反射来访问。
  2. 修改 statement 属性中的 WHERE 部分: 将需要拼接的 SQL 代码添加到 WHERE 条件的末尾,这样确保能够和其他$query构建的查询条件结合使用。
<?php
// 在 Repository 中

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use Doctrine\DBAL\Query\QueryBuilder;
use ReflectionClass;

class YourRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
    public function findByLocation(float $lat, float $lng, float $radius): QueryResultInterface
    {
        $query = $this->createQuery();

        $where[] = $query->equals('deleted', 0);

        // 其他现有的查询条件
        $where[] = $query->greaterThan('crdate', strtotime('-30 days'));
        $where[] = $query->equals('active', true);


        $distanceSql = "acos(sin(:lat)*sin(radians(lat)) + cos(:lat)*cos(radians(lat))*cos(radians(lng)-:lng)) * 6371 < :radius";

       $statementParameters = [
            ':lat' => $lat,
            ':lng' => $lng,
            ':radius' => $radius,
       ];
       //这里会生成 $query,包括 $query 里定义的条件和 placeholder,最后被 Doctrine 查询
        $queryBuilder = GeneralUtility::makeInstance(QueryBuilder::class);
        $queryBuilder->from($this->objectType);

        $whereStatement = $query->buildQueryFromConstraints($queryBuilder, $where);
        $finalStatement =  $whereStatement->sql()." AND ".$distanceSql;
        
        $reflect = new ReflectionClass($query);
        $statementProp = $reflect->getProperty('statement');
        $statementProp->setAccessible(true);
        
        $statementProp->setValue($query,$finalStatement);

         $query->getQuerySettings()->setRespectStoragePage(false);
         
         return $query->execute($statementParameters);


    }

}

操作步骤:

  1. 将上述代码复制到你自定义的 Repository 类中,替换类名为实际的 Repository 名称,方法名为你需要调用半径搜索方法的方法名,根据需求调整代码。

  2. 在需要进行半径搜索的位置,调用 findByLocation() 方法,传入经度、纬度和半径等参数,将获得满足条件的实体对象。
    解释
    首先构建通用的 Extbase 的查询条件。
    再定义半径搜索的SQL语句$distanceSql
    再把条件和SQL片段通过 Doctrine的QueryBuilder结合起来,生成最后执行的 SQL,再把此SQL设置到Extbase的 Query 的 statement。

    在execute的时候设置查询参数,并且关闭storagePage过滤。

这种方式的优势是能与 Extbase 的标准查询条件进行良好集成。重要提示:在直接操作 SQL 语句时,要非常注意SQL注入风险。 请确保你已正确过滤并转义所有的用户输入参数 ,防止出现安全隐患。

2. 使用 Doctrine DQL (如果使用的是 TYPO3 v12)

如果使用的是TYPO3 v12,可以使用 Doctrine DQL (Doctrine Query Language) 进行更高级的查询构建。DQL 更贴近 OOP,并且可以通过参数绑定来提高安全。

在 v12 使用 DQL 需要先配置模型, 主要是需要把模型的字段映射成数据表的列,方法如下,
参考文档: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Database/Dql/

model:
在  <yourextkey>/Configuration/Extbase/Persistence/Classes.php 文件中进行如下映射配置:

<?php
declare(strict_types=1);

return [
    'Vendor\\Yourextkey\\Domain\\Model\\YourModel' => [
        'tableName' => 'tx_yourextkey_domain_model_yourmodel',
         'properties' => [
                'uid' => [
                'fieldName' => 'uid',
            ],
                'lat' => [
                 'fieldName' => 'lat'
               ],
                 'lng' => [
                  'fieldName' => 'lng'
                ]

             // Add more attributes
             // if you need them
           ],
    ],
];

接下来可以使用 createQuery 设置 DQL :

 <?php
// 在 Repository 中

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use Doctrine\ORM\Query\ResultSetMapping;


class YourRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
    public function findByLocationWithDql(float $lat, float $lng, float $radius): QueryResultInterface
    {

        $query = $this->createQuery();

         $where = $query->getQuerySettings();
        //禁用存储页面限制
        $where->setRespectStoragePage(false);

        $dql = 'SELECT e FROM Vendor\Yourextkey\Domain\Model\YourModel e 
                    WHERE
                       acos(sin(:lat)*sin(radians(e.lat)) + cos(:lat)*cos(radians(e.lat))*cos(radians(e.lng)-:lng)) * 6371 < :radius AND
                      e.deleted = 0
                      AND e.active = true
                 ';

          $doctrineQuery = $this->entityManager->createQuery($dql);


        $doctrineQuery->setParameter('lat',$lat);
        $doctrineQuery->setParameter('lng',$lng);
        $doctrineQuery->setParameter('radius',$radius);

      return $doctrineQuery->getResult();



    }

}

操作步骤:

  1. <yourextkey>/Configuration/Extbase/Persistence/Classes.php 中进行模型映射配置
  2. 将上述代码复制到你自定义的 Repository 类中,替换类名为实际的 Repository 名称,方法名为你需要调用半径搜索方法的方法名,根据需求调整代码。
  3. 在需要进行半径搜索的位置,调用 findByLocationWithDql() 方法,传入经度、纬度和半径等参数,将获得满足条件的实体对象。
    解释
    先配置 Classes.php 文件,进行模型属性的表列映射。然后写出符合DQL的查询语句。 最后,使用entityManager 去创建查询对象和绑定查询参数,然后返回执行结果。

这种方式具有更高的可读性,性能较好,代码易于维护。但要求你对DQL 有所了解,配置较为繁琐。

结语

以上两种方法提供了在 Extbase 中添加自定义 SQL WHERE 部分的有效途径,并处理了涉及地理位置半径查询的情况。每种方法都有其独特的优点和适用场景。开发者可以根据项目需求,选择最合适的方法。请始终牢记代码安全和性能优化的重要性,谨慎使用自定义 SQL 并确保代码的可维护性。