Extbase自定义 WHERE 查询:解决复杂 SQL 条件
2025-01-13 16:54:45
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()
方法,但是你可以借助其他方式做到。
以下示例演示了具体的操作步骤:
- 获取
Query
的statement
属性:Query
对象有一个受保护的属性,但可以通过反射来访问。 - 修改
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);
}
}
操作步骤:
-
将上述代码复制到你自定义的 Repository 类中,替换类名为实际的 Repository 名称,方法名为你需要调用半径搜索方法的方法名,根据需求调整代码。
-
在需要进行半径搜索的位置,调用
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();
}
}
操作步骤:
- 在
<yourextkey>/Configuration/Extbase/Persistence/Classes.php
中进行模型映射配置 - 将上述代码复制到你自定义的 Repository 类中,替换类名为实际的 Repository 名称,方法名为你需要调用半径搜索方法的方法名,根据需求调整代码。
- 在需要进行半径搜索的位置,调用
findByLocationWithDql()
方法,传入经度、纬度和半径等参数,将获得满足条件的实体对象。
解释
先配置Classes.php
文件,进行模型属性的表列映射。然后写出符合DQL
的查询语句。 最后,使用entityManager
去创建查询对象和绑定查询参数,然后返回执行结果。
这种方式具有更高的可读性,性能较好,代码易于维护。但要求你对DQL
有所了解,配置较为繁琐。
结语
以上两种方法提供了在 Extbase 中添加自定义 SQL WHERE
部分的有效途径,并处理了涉及地理位置半径查询的情况。每种方法都有其独特的优点和适用场景。开发者可以根据项目需求,选择最合适的方法。请始终牢记代码安全和性能优化的重要性,谨慎使用自定义 SQL 并确保代码的可维护性。