如何实现可组合查询:10k 英尺视图
不难意识到,为了实现这一点,被链接的方法必须增量设置一些数据结构,最终由执行最终查询的某个方法解释。但是对于如何编排这件事,有一定程度的自由度。
示例代码是
$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');
我们在这里看到了什么?
- 有某种类型,它
$db
是一个实例,它至少公开了一个select
方法。请注意,如果您希望能够完全重新排序调用,则此类型需要公开具有所有可能参与调用链的签名的方法。
- 每个链接的方法都返回一个实例,该实例暴露了所有相关签名的方法;这可能与 . 的类型相同,也可能不同
$db
。
- 在收集到“查询计划”之后,我们需要调用一些方法来实际执行它并返回结果(我将称之为物化查询的过程)。由于显而易见的原因,这个方法只能是调用链中的最后一个,但在这种情况下,最后一个方法是
order
,这似乎不太正确:毕竟我们希望能够在调用链中更早地移动它。让我们记住这一点。
因此,我们可以将发生的事情分解为三个不同的步骤。
第 1 步:开始
我们确定至少需要一种类型来收集有关查询计划的信息。假设类型如下所示:
interface QueryPlanInterface
{
public function select(...);
public function limit(...);
// etc
}
class QueryPlan implements QueryPlanInterface
{
private $variable_that_points_to_data_store;
private $variables_to_hold_query_description;
public function select(...)
{
$this->encodeSelectInformation(...);
return $this;
}
// and so on for the rest of the methods; all of them return $this
}
QueryPlan
需要适当的属性来记住它不仅应该产生什么查询,而且还要记住将该查询定向到哪里,因为它是这种类型的实例,您将在调用链的末端获得;为了实现查询,这两条信息都是必需的。我还提供了一种QueryPlanInterface
类型;其意义将在稍后阐明。
这是否意味着它$db
是 type QueryPlan
?乍一看,您可能会说是,但经过仔细检查,这种安排开始出现问题。最大的问题是陈旧状态:
// What would this code do?
$db->limit(2);
// ...a little later...
$albums = $db->select('albums');
这要检索多少张专辑?因为我们没有“重置”查询计划,所以它应该是 2。但从最后一行看,这一点都不明显,读起来很不一样。这是一个糟糕的安排,可能会导致不必要的错误。
那么如何解决这个问题呢?一种选择是select
重置查询计划,但这会遇到相反的问题:$db->limit(1)->select('albums')
现在选择所有专辑。这看起来不太好。
选项将是通过安排第一次调用返回一个新QueryPlan
实例来“启动”链。这样,每条链都在单独的查询计划上运行,虽然您可以一点一点地编写查询计划,但您不能再意外地做到这一点。所以你可以有:
class DatabaseTable
{
public function query()
{
return new QueryPlan(...); // pass in data store-related information
}
}
它解决了所有这些问题,但要求您始终->query()
在前面写:
$db->query()->limit(1)->select('albums');
如果你不想接到这个额外的电话怎么办?在这种情况下,类DatabaseTable
也必须实现,不同之处在于实现每次QueryPlanInterface
都会创建一个新的:QueryPlan
class DatabaseTable implements QueryPlanInterface
{
public function select(...)
{
$q = new QueryPlan();
return $q->select(...);
}
public function limit(...)
{
$q = new QueryPlan();
return $q->limit(...);
}
// and so on for the rest of the methods
}
你现在可以$db->limit(1)->select('albums')
毫无问题地写作了;这种安排可以描述为“每次编写时,$db->something(...)
您都开始编写一个独立于所有先前和未来查询的新查询”。
第 2 步:链接
这实际上是最简单的部分;我们已经看到了QueryPlan
总是return $this
启用链接的方法。
第 3 步:具体化
我们仍然需要某种方式来表达“好的,我已经完成了作曲;给我结果”。为此目的,完全可以使用专用方法:
interface QueryPlanInterface
{
// ...other methods as above...
public function get(); // this executes the query and returns the results
}
这使您可以编写
$anAlbum = $db->limit(1)->select('albums')->get();
这个解决方案没有错,也没有错:很明显,实际查询是在什么时候执行的。但是这个问题使用了一个看起来不像那样工作的例子。有可能实现这样的语法吗?
答案是肯定的和否定的。是的,因为它确实是可能的,但从某种意义上说,所发生的事情的语义必须改变。
PHP 没有使方法能够“自动”调用的工具,因此必须有一些东西可以触发物化,即使乍一看这东西看起来不像是方法调用。但是什么?好吧,想想最常见的用例可能是什么:
$albums = $db->select('albums'); // no materialization yet
foreach ($albums as $album) {
// ...
}
这可以工作吗?当然,只要QueryPlanInterface
extends IteratorAggregate
:
interface QueryPlanInterface extends IteratorAggregate
{
// ...other methods as above...
public function getIterator();
}
这里的想法是foreach
触发对 的调用getIterator
,这反过来将创建另一个类的实例,该类注入了实现QueryPlanInterface
已编译的所有信息。该类将在现场执行实际查询,并在迭代过程中按需具体化结果。
我选择了实现IteratorAggregate
,而不是Iterator
具体地,以便迭代状态可以进入一个新实例,这允许对同一个查询计划的多次迭代并行进行而不会出现问题。
最后,这个foreach
技巧看起来很简洁,但其他常见用例(将查询结果放入数组)呢?我们让它变得笨拙了吗?
不是真的,感谢iterator_to_array
:
$albums = iterator_to_array($db->select('albums'));
结论
这是否需要编写大量代码?当然。我们有DatabaseTable
, QueryPlanInterface
,QueryPlan
本身以及QueryPlanIterator
我们已经描述但未显示的。此外,这些类聚合的所有编码状态可能需要保存在更多类的实例中。
这值得么?很有可能。这是因为这种解决方案提供:
- 具有清晰语义的有吸引力的流畅接口(可链接调用)(每次启动时,您都开始描述独立于任何其他查询的新查询)
- 将查询接口与数据存储解耦(每个实例都
QueryPlan
在抽象数据存储上保留一个句柄,因此理论上您可以使用相同的语法查询从关系数据库到平面文本文件的任何内容)
- 可组合性(您可以现在开始编写 a
QueryPlan
并在将来继续这样做,即使是在另一种方法中)
- 可重用性(您可以
QueryPlan
多次实现每个)
根本不是一个坏包。