如何创建一个流畅的查询界面? [英] How to create a fluent query interface?

查看:52
本文介绍了如何创建一个流畅的查询界面?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I know how to chain class methods (with the "return $this" and all), but what i am trying to do is to chain them in a smart way, have a look at this:

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

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

What i could understand from this code sample is that the first 3 methods (select, where, limit) build the query statement that will be executed, and the last one (order) comes to finish the statement and then executes it and throws back the result, right ?

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

But, this isn't the case, because i can easily drop any of these methods (except "select" of course) or - more importantly - change their order and nothing will go wrong !! That means that the method "select" handles the work, right ? Then how the other 3 methods add to/affect the query statement after the method "select" was already called !??

推荐答案

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

不难意识到,要实现这一目标,被链接的方法必须逐步建立一些数据结构,该数据结构最终将由执行最终查询的某种方法来解释.但是,关于如何进行精心安排,还是有一定程度的自由度.

How to implement composable queries: the 10k feet view

It's not difficult to realize that in order to achieve this the methods being chained must incrementally set up some data structure which is finally being interpreted by some method that executes the final query. But there are some degrees of freedom regarding exactly how this can be orchestrated.

示例代码为

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

我们在这里看到什么?

  1. 有些类型是$db的实例,它至少公开了select方法.请注意,如果您希望能够完全重新排序调用,则此类型需要公开带有可能参与调用链的所有签名的方法.
  2. 每个链接方法都返回某个实例的实例,该实例公开了所有相关签名的方法;此类型可能与$db相同,也可能不同.
  3. 收集查询计划"后,我们需要调用某种方法来实际执行它并返回结果(该过程将被称为具体化查询).出于明显的原因,此方法只能是调用链中的最后一个方法,但是在这种情况下,最后一个方法是order,这似乎不合适:我们希望能够将其移到调用链中的更早位置.让我们牢记这一点.
  1. There is some type, which $db is an instance of, that exposes at least a select method. Note that if you want to be able to fully reorder the calls this type needs to expose methods with all of the possible signatures that can take part in the call chain.
  2. Each of the chained methods returns an instance of something that exposes methods will all relevant signatures; this may or may not be the same type as $db.
  3. After the "query plan" has been collected, we need to call some method to actually execute it and return the results (a process which I am going to call materializing the query). This method can only be the last one in the call chain for obvious reasons, but in this case the last method is order, which does not seem right: we want to be able to move it earlier in the chain after all. Let's keep this in mind.

因此,我们可以按三个不同的步骤来分解发生的情况.

Therefore we can break down what happens in three distinct steps.

我们确定至少需要一种类型来收集有关查询计划的信息.假设类型如下:

We established that there needs to be at least one type that collects information about the query plan. Let's assume the type looks like this:

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类型;其意义将在稍后阐明.

QueryPlan needs appropriate properties to remember not only what query it should produce, but also where to direct that query because it is an instance of this type you will have on hand at the end of the call chain; both pieces of information are necessary in order for the query to be materialized. I have also provided a QueryPlanInterface type; its significance will be made clear later on.

这是否意味着$dbQueryPlan类型?乍一看,您可能会说是",但经过仔细检查后,这种安排开始出现问题.最大的问题是陈旧状态:

Does this mean that $db is of type QueryPlan? At first sight you might say yes, but upon closer inspection issues start to arise from such an arrangement. The biggest problem is stale state:

// What would this code do?
$db->limit(2);

// ...a little later...
$albums = $db->select('albums');

这将检索多少张专辑?由于我们没有重置"查询计划,因此它应该为2.但这从最后一行完全不明显,因为最后一行的读法非常不同.这是一个糟糕的安排,可能导致不必要的错误.

How many albums is this going to retrieve? Since we did not "reset" the query plan it should be 2. But that's not obvious at all from the last line, which reads very differently. This is a bad arrangement that can lead to unnecessary bugs.

那么如何解决这个问题呢?一个选项是select重设查询计划,但这遇到了相反的问题:$db->limit(1)->select('albums')现在选择所有专辑.看起来不太好.

So how to solve this problem? One option would be for select to reset the query plan, but this runs into the opposite issue: $db->limit(1)->select('albums') now selects all albums. This doesn't look nice.

该选项是通过安排第一次调用以返回新的QueryPlan实例来启动"该链.这样,每个链都可在单独的查询计划上运行,并且尽管您可以一点一点地编写查询计划,但您再也不能偶然地这样做了.因此,您可以:

The option would be to "kick off" the chain by arranging for the first call to return a new QueryPlan instance. This way each chain operates on a separate query plan, and while you can compose a query plan bit by bit you can no longer do it by accident. So you could have:

class DatabaseTable
{
    public function query()
    {
        return new QueryPlan(...); // pass in data store-related information
    }
}

解决了所有这些问题,但要求您始终在前面写->query():

which solves all these problem but requires you to always write ->query() in front:

$db->query()->limit(1)->select('albums');

如果您不想打这个额外电话怎么办?在这种情况下,类DatabaseTable也必须实现QueryPlanInterface,不同之处在于该实现每次都会创建一个新的QueryPlan:

What if you don't want to have this extra call? In that case class DatabaseTable has to implement QueryPlanInterface as well, with the difference that the implementation will create a new QueryPlan each time:

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(...)时,您都会开始编写一个独立于所有先前和将来查询的新查询".

You can now write $db->limit(1)->select('albums') without any problem; the arrangement can be described as "each time you write $db->something(...) you start composing a new query that is independent of all previous and future ones".

这实际上是最简单的部分;我们已经看到QueryPlan中的方法始终如何return $this启用链接.

This is actually the easiest part; we already saw how the methods in QueryPlan always return $this to enable chaining.

我们仍然需要某种方式说好,我已经完成写作;给我结果".为此,完全有可能使用专用方法:

We still need some way to say "OK, I 'm done composing; get me the results". It is perfectly possible to use a dedicated method for this purpose:

interface QueryPlanInterface
{
    // ...other methods as above...
    public function get(); // this executes the query and returns the results
}

这使您可以编写

$anAlbum = $db->limit(1)->select('albums')->get();

此解决方案没有错,也有很多权利:显而易见,在该位置执行实际查询.但问题是使用了一个似乎并非如此的示例.可以实现这种语法吗?

There is nothing wrong and lots of right with this solution: it's obvious at which point the actual query is executed. But the question uses an example that does not appear to work like that. Is it possible to achieve such syntax?

答案是是和不是.是的,因为确实有可能,但是从某种意义上说,发生的事情的语义必须改变.

The answer is yes and no. Yes in that it is indeed possible, but no in the sense that the semantics of what happens will have to change.

PHP没有使自动"调用方法的功能,因此必须有某些东西触发实现,即使某些东西乍一看并不像方法调用一样.但是呢好吧,请考虑一下最常见的用例:

PHP has no facility that enables a method to be "automatically" called, so there has to be something that triggers the materialization, even if that something does not look like a method call at first sight. But what? Well, think about what is perhaps the most common use case:

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

可以使它起作用吗?当然,只要QueryPlanInterface扩展 IteratorAggregate :

Can this be made to work? Sure, as long as QueryPlanInterface extends IteratorAggregate:

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

这里的想法是foreach触发对getIterator的调用,这又将创建另一个类的实例,该类注入了QueryPlanInterface实现的所有已编译信息.此类将在现场执行实际查询,并在迭代过程中按需实现结果.

The idea here is that the foreach triggers a call to getIterator, which in turn will create an instance of yet another class injected with all the information that the implementation of QueryPlanInterface has compiled. This class will execute the actual query on the spot and materialize the results on demand during the iteration.

我选择专门实现IteratorAggregate而不是Iterator,以便迭代状态可以进入一个新实例,该实例允许对同一查询计划进行多次迭代而不会出现问题.

I have chosen to implement IteratorAggregate and not Iterator specifically so that the iteration state can go into a new instance, which allows multiple iterations over the same query plan to go on in parallel without problems.

最后,这个foreach技巧看起来很整洁,但是其他常见用例(将查询结果放入数组中)又如何呢?我们使它变得笨拙了吗?

Finally, this foreach trick looks neat but what about the other common use case (getting the query results into an array)? Have we made that unwieldy?

并非如此,这要感谢 iterator_to_array :

Not really, thanks to iterator_to_array:

$albums = iterator_to_array($db->select('albums'));

结论

这是否需要编写大量代码?当然.我们有DatabaseTableQueryPlanInterfaceQueryPlan本身,还有我们已经描述但未显示的QueryPlanIterator.此外,这些类汇总的所有编码状态可能都需要保留在更多类的实例中.

Conclusion

Does this require lots of code to be written? For sure. We have DatabaseTable, QueryPlanInterface, QueryPlan itself and also the QueryPlanIterator we have described but not shown. In addition, all the encoded state that these classes aggregate will probably need to be kept in instances of yet more classes.

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

Is it worth it? Quite likely. That's because this kind of solution offers:

  • 具有清晰语义的引人入胜的流畅接口(可链接调用)(每次启动时,您都开始描述独立于其他任何查询的新查询)
  • 将查询接口与数据存储区分离(每个QueryPlan实例在抽象数据存储区上保留一个句柄,因此,理论上您可以使用相同的语法查询从关系数据库到平面文本文件的所有内容)
  • 可组合性(您可以立即开始编写QueryPlan,将来甚至可以使用其他方法继续进行编写)
  • 可重用性(您可以多次实现每个QueryPlan)
  • an attractive fluent interface (chainable calls) with clear semantics (each time you kick off you start describing a new query independent of any other)
  • decoupling of the query interface from the data store (each instance of QueryPlan keeps a handle on an abstract data store, so you can theoretically query anything from relational databases to flat text files using the same syntax)
  • composability (you can start composing a QueryPlan now and continue doing so in the future, even in another method)
  • reusability (you can materialize each QueryPlan more than once)

一点都不差劲的包.

这篇关于如何创建一个流畅的查询界面?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆