11

我知道如何链接类方法(使用“return $this”和所有方法),但我想做的是以一种聪明的方式链接它们,看看这个:

$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');

我从这个代码示例中可以理解的是,前 3 个方法(select、where、limit)构建将要执行的查询语句,最后一个(order)来完成语句,然后执行它并返回结果,对吧?

但是,事实并非如此,因为我可以轻松地放弃这些方法中的任何一种(当然“选择”除外),或者 - 更重要的是 - 更改它们的顺序并且不会出错!这意味着“选择”方法处理工作,对吗?那么在方法“select”已经被调用之后,其他 3 个方法如何添加/影响查询语句!??

4

3 回答 3

35

如何实现可组合查询:10k 英尺视图

不难意识到,为了实现这一点,被链接的方法必须增量设置一些数据结构,最终由执行最终查询的某个方法解释。但是对于如何编排这件事,有一定程度的自由度。

示例代码是

$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');

我们在这里看到了什么?

  1. 有某种类型,它$db是一个实例,它至少公开了一个select方法。请注意,如果您希望能够完全重新排序调用,则此类型需要公开具有所有可能参与调用链的签名的方法。
  2. 每个链接的方法都返回一个实例,该实例暴露了所有相关签名的方法;这可能与 . 的类型相同,也可能不同$db
  3. 在收集到“查询计划”之后,我们需要调用一些方法来实际执行它并返回结果(我将称之为物化查询的过程)。由于显而易见的原因,这个方法只能是调用链中的最后一个,但在这种情况下,最后一个方法是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) {
    // ...
}

这可以工作吗?当然,只要QueryPlanInterfaceextends 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在抽象数据存储上保留一个句柄,因此理论上您可以使用相同的语法查询从关系数据库到平面文本文件的任何内容)
  • 可组合性(您可以现在开始编写 aQueryPlan并在将来继续这样做,即使是在另一种方法中)
  • 可重用性(您可以QueryPlan多次实现每个)

根本不是一个坏包。

于 2013-07-01T21:30:24.223 回答
1

这需要一个非常优雅的解决方案。

与其重新发明轮子,不如研究现有的框架。

我建议 Laravel 使用 Eloquent ORM。您将能够做到这一点以及更多。

于 2013-07-01T19:47:56.130 回答
0

您可能需要一个启动实际查询的方法,而这些方法喜欢select并且order_by只存储该点的信息。

如果您实现Iterator接口,并且第一次运行查询rewind或被current命中(想想foreach)或Countable,您可以将其设为隐式,因此可以通过调用count()对象来生成结果计数。我个人不喜欢使用像这样构建的库,我更可能喜欢显式调用,这样我就可以看到查询触发运行的位置。

于 2013-07-01T19:53:11.823 回答