如何创建流畅的查询界面?

时间:2013-07-01 19:46:13

标签: php oop fluent-interface

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

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

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

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

3 个答案:

答案 0 :(得分:34)

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

不难发现,为了实现这一点,被链接的方法必须逐步建立一些数据结构,最终由执行最终查询的某种方法解释。但是对于如何精心策划这一点有一定程度的自由。

示例代码是

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

我们在这看到什么?

  1. 有一些类型,$db是其实例,至少公开select方法。请注意,如果您希望完全重新排序调用,则此类型需要公开包含可以参与调用链的所有可能签名的方法。
  2. 每个链接方法返回一个事件的实例,该方法将方法暴露给所有相关的签名;这可能与$db类型相同,也可能不同。
  3. "查询计划"已经收集了,我们需要调用一些方法来实际执行它并返回结果(我将调用实现查询的过程)。由于显而易见的原因,此方法只能是调用链中的最后一个方法,但在这种情况下,最后一个方法是order,这似乎不对:我们希望能够在链中提前移动它。让我们记住这一点。
  4. 因此,我们可以分解在三个不同步骤中发生的事情。

    第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属于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没有任何工具可以使方法自动地#34;调用,所以必须有某些东西才能触发实现,即使这些东西看起来不像一个方法调用。但是什么?好吧,想想可能是最常见的用例:

    $albums = $db->select('albums'); // no materialization yet
    foreach ($albums as $album) {
        // ...
    }
    

    这可以使用吗?当然,只要QueryPlanInterface延长IteratorAggregate

    interface QueryPlanInterface extends IteratorAggregate
    {
        // ...other methods as above...
        public function getIterator();
    }
    

    这里的想法是foreach触发对getIterator的调用,而QueryPlanInterface又会创建另一个类的实例,该类注入了IteratorAggregate的实现所具有的所有信息编译。该类将在现场执行实际查询,并在迭代期间按需实现结果。

    我选择专门实现Iterator而不是foreach,以便迭代状态可以进入一个新实例,这允许在同一查询计划上进行多次迭代并行而不会出现问题。

    最后,这个$albums = iterator_to_array($db->select('albums')); 技巧看起来很整洁但是另一个常见的用例(将查询结果变成数组)呢?我们做得那么笨重吗?

    不是真的,感谢iterator_to_array

    DatabaseTable

    结论

    这需要编写大量代码吗?当然。我们有QueryPlanInterfaceQueryPlanQueryPlanIterator本身以及我们已描述但未显示的QueryPlan。此外,这些类聚合的所有编码状态可能需要保存在更多类的实例中。

    值得吗?很有可能。那是因为这种解决方案提供了:

    • 一个有吸引力的流畅界面(可链接的调用),具有清晰的语义(每次你开始描述一个独立于任何其他查询的新查询)
    • 将查询接口与数据存储分离(QueryPlan的每个实例都保留抽象数据存储的句柄,因此理论上可以使用相同的语法查询从关系数据库到平面文本文件的任何内容)
    • 可组合性(您现在可以开始编写QueryPlan并在将来继续这样做,即使是在另一种方法中也是如此)
    • 可重用性(您可以多次实现每个{{1}})

    根本不是一个糟糕的包裹。

答案 1 :(得分:1)

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

不要重新发明轮子,而是要查看现有的框架。

我建议Laravel使用Eloquent ORM。你将能够做到这一点,还有更多。

答案 2 :(得分:0)

您可能需要一种方法来启动实际查询,而像selectorder_by这样的方法只会将信息存储到该点。

如果你实现了Iterator界面,并且第一次rewindcurrent被点击时运行查询(想想foreach),你可以隐含这一点,或者Countable,因此可以通过使用对象调用count()来生成结果计数。我个人不想使用这样构建的库,我更可能欣赏一个显式调用,这样我就可以看到查询触发的位置。