我知道如何链接类方法(使用“return $ this”和all),但我想要做的是以聪明的方式链接它们,看看这个:
$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');
我从这个代码示例中可以理解的是,前3个方法(select,where,limit)构建了将要执行的查询语句,最后一个(order)来完成语句然后执行它抛出结果吧?
但事实并非如此,因为我可以轻松放弃任何这些方法(当然除了“选择”)或者 - 更重要的是 - 改变他们的顺序,什么都不会出错!!这意味着方法“选择”处理工作,对吗?那么在调用方法“select”之后,其他3个方法如何添加/影响查询语句!
答案 0 :(得分:34)
不难发现,为了实现这一点,被链接的方法必须逐步建立一些数据结构,最终由执行最终查询的某种方法解释。但是对于如何精心策划这一点有一定程度的自由。
示例代码是
$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');
我们在这看到什么?
$db
是其实例,至少公开select
方法。请注意,如果您希望完全重新排序调用,则此类型需要公开包含可以参与调用链的所有可能签名的方法。$db
类型相同,也可能不同。order
,这似乎不对:我们希望能够在链中提前移动它。让我们记住这一点。因此,我们可以分解在三个不同步骤中发生的事情。
我们确定至少需要一种类型来收集有关查询计划的信息。让我们假设类型如下:
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(...)
时,你都会开始编写一个独立于所有先前和未来的查询的新查询"。
这实际上是最容易的部分;我们已经看到QueryPlan
中的方法总是return $this
来启用链接。
我们仍然需要某种方式来说"好的,我已经完成了作曲;给我结果"。为此目的完全可以使用专用方法:
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
这需要编写大量代码吗?当然。我们有QueryPlanInterface
,QueryPlan
,QueryPlanIterator
本身以及我们已描述但未显示的QueryPlan
。此外,这些类聚合的所有编码状态可能需要保存在更多类的实例中。
值得吗?很有可能。那是因为这种解决方案提供了:
QueryPlan
的每个实例都保留抽象数据存储的句柄,因此理论上可以使用相同的语法查询从关系数据库到平面文本文件的任何内容)QueryPlan
并在将来继续这样做,即使是在另一种方法中也是如此)根本不是一个糟糕的包裹。
答案 1 :(得分:1)
这需要一个非常优雅的解决方案。
不要重新发明轮子,而是要查看现有的框架。
我建议Laravel使用Eloquent ORM。你将能够做到这一点,还有更多。
答案 2 :(得分:0)