当下所有包含数据库组件的框架,都提供了一套流畅的操作方式去生成查询语句,这一部分我们称作 查询构造器 。查询构造器的存在,使得在数据库操作这一层面彻底的和原生开发区分开来。原生开发中,查询语句都是人为地根据业务需求手工写的,没有对查询语句进行规范的封装(即使有也不是人人能够做得到且做得好),随着项目扩展,这种原生的写法会导致项目愈发难以维护,而且由于操作原始,更加容易引发很多不必要的 BUG 以及安全隐患。查询构造器针对每一类语句关键字进行封装,通过流畅的链式方法组合,形成一个树状的数据结构,最终由生成器生成目标查询语句。
我向来认为 Laravel 框架的组件永远不是最优秀的,但由于其他框架或多或少不优雅、实现差、功能残缺,才使得 Laravel 的每个组件无论是单个拿出来看还是组合起来看,都是那么的好用,这其中的典型就是 查询构造器。
大多数优秀框架提供的查询构造器 功能 都已经十分完善,基本上在绝大多数业务上可以替换手工书写查询语句,但是对于复杂的查询语句,很多框架的查询构造器提供的方法要么是根本无法实现(比如多种条件和条件之间的逻辑运算、嵌套查询或子查询),要么是实现的方式不够优雅和直观,使得在对于复杂的查询条件的情况下,不得不回归原生的 SQL 语句查询。虽然一部分人认为原生的查询语句才是最直观且合适的,亦或者认为复杂的查询语句就应当使用原生,但是我们不应该孤立在某一类项目或者一类人群下看待这个问题。要知道,对于大型项目,合理的封装和可维护性,开发的便捷和效率这些因素往往十分重要,查询构造器的诞生本来就是因为这个原因。
Laravel 给出了一个令人满意的实现方法,使得之前那些框架的查询构造器黯然失色(当然,不包括 Doctrine)。
清晰地结构
若是不看文档和任何介绍,你能够得出下面查询构造器最终生成的 SQL 语句或者看得出这段代码的意图吗:
php $collection = DB :: table ( 'articles' ) -> select ([ 'title' , 'name' , 'author' , 'created_at' , 'published_at' ]) -> where ( 'name' , 'like' , "%$name%" ) -> where ( function ( $query ) { $query -> where ( 'top' , '>' , 5 ) -> orWhere ( 'status' , '=' , 1 ); }) -> orderBy ( 'updated_at' , 'desc' ) -> take ( 5 ) -> get ()
相信很多人都非常容易看得出这个代码的含义,上述代码仅仅只是个例子,若移步至官方文档和查询构造器的 API 文档,还有更多的方法可以使用。
Laravel 的查询构造器基本涵盖了日常所用到的所有语句的可能,也提供了很多有用的封装,用于细粒的操作。这些大多在文档里体现,本文不作重点讲述。
复杂逻辑下的思维
我不是文档的搬运工,再详尽的开发教程也永远教不会那些只会复制粘贴、乱问问题的人,作为一枚不断学习的 PHPer,只会用永远不够,任何一个易用的框架和组件,思想永远是首要学习的东西。但愿认真看完以下章节和后续文章的 PHPer 不再会问出一些愚蠢的问题(当然,没能一下子懂并不是愚蠢,也可能是……你和我的思路不一致,稍微转换思维或许会发现新的有趣的世界)。
我第一次使用查询构造器是在运用 ThinkPHP 3.1 开发项目时用到的,当时笔者水平很烂,为了加强学习,我忍着枯燥无味,将 TP 的数据库组件的源代码一行一行的吃了个透。虽然现在来看 TP 的实现并不是很优秀,但是依旧给了我无限的帮助,使得我阅读源码的能力提升了一个很大的台阶。
回过头来,有了之前的经验,我依旧选择阅读源码的方式来学习框架,而不仅仅只依靠文档,因为我更想知道作者的思路和每一个功能实现的方式,以及为什么会这么用。
我对比了很多个数据库组件的实现,包括著名的 Doctrine,在很多地方都有大同小异,当然 Laravel 的也不例外,不过一旦考究起细节,才不得不赞叹,越是国际化的认可度高的,确实有它值得认可的道理。
查询构造器都会提供一种 Chain(链式) 访问的方法,这样使得开发者可以更为直观的对应目标查询语句,因此方法名往往和 SQL 语句差距不大(NoSQL 驱动的存在特殊性在此不做讨论),但是查询构造器本身的目的是为了生成查询语句,因此如何组织这些方法的参数,保证在易于理解和记忆的情况下,又能有着无比强大的功能,便成了考究设计者水平的东西。
在一些早期框架,查询构造器一般都会提供 where、order by、group by 等很直观的的语句对应的方法,尤其是 order by 这类参数结构单一的,十分容易设计,比如第一个参数是排序的字段,后者则是排序方式,亦或者参数是一个数组,数组下有很多项,这样就可以生成多重排序。
不过一旦到了 where 这种结构复杂的情况,传统的思路就容易吃瘪,比如多个条件如何体现?传统的代码如下
$query -> where ([ 'a' => 'foo' , 'b' => 'bar' ]);
这种语句的 where 最终是这样 WHERE a = 'foo' AND b = 'bar',但是如果我们要这样 WHERE a = 'foo' OR b = 'bar' 或者 WHERE a > 100 AND b = 'foo' 上面的那种通过数组来组织的方式,就很难实现。
查询构造器想要实现上面例子的这个 where 方法,实际上只需要循环参数提供的数组,记录到查询构造器对象的一个私有属性里,在最终生成输出 SQL 语句的阶段,进行拼接即可。可以自己尝试着实现一个简单的查询构造器。
通过数组组织等式,很容易,但数组结构本身存在局限,而且一旦结构复杂,可读性便会极大地降低,框架的本质是提供一种方便的机制来提升开发效率降低错误概率,这样很显然背道而驰。有的框架提出了另外一种方案,在不破坏整体的链式访问的结构下,在局部使用原生语句来实现复杂的查询:
$query -> where ( "a > 100 OR b = 'foo'" );
这种思路是一个不错的解决办法,但是就像我之前提到的,这种方式虽然比直接全盘原生语句好了些,但不可避免这种写法无法过滤,一旦用于查询判断的条件式中有外部变量,就很容易出现注入问题,或许你可以说利用 PDO,以以下方式实现:
$query -> where ( "a > ? OR b = ?" , [ $parameter1 , $parameter2 ]);
但是如果你是框架或组件的作者,你一定会苦恼于另外一件事:这个方法如何适用于上述各种情况(比如简单条件下的、复杂条件下的等等)?有的人想到了判断参数类型来实现,比如参数 0 是数组时,就用常规的办法(上面的 blockquote 有讲),若参数 0 是文本,就用原生的方式集成。
TP 就是这么一个思路:
protected function parseWhere ( $where ) { $whereStr = '' ; if ( is_string ( $where )) { // 直接使用字符串条件 $whereStr = $where ; } else { // 使用数组表达式 $operate = isset ( $where [ '_logic' ]) ? strtoupper ( $where [ '_logic' ]) : '' ; if ( in_array ( $operate , array ( 'AND' , 'OR' , 'XOR' ))) { // 定义逻辑运算规则 例如 OR XOR AND NOT $operate = ' ' . $operate . ' ' ; unset ( $where [ '_logic' ]); } else { // 默认进行 AND 运算 $operate = ' AND ' ; } // 更多请参考 https://github.com/top-think/thinkphp/blob/master/ThinkPHP/Library/Think/Db/Driver.class.php#L562
好了,重点来了,我说过这种方式不是不好,而是还可以更好,因为复合条件存在的可能非常多,基本是常态了,因此我认为常态的形式,就应当进行 简化 和摆脱原生写法的困扰,这样才更为合理。Laravel 封装的这一系列方法,更为 优雅 ,相信各位能够感觉得到,因为你无须过多参阅文档,都能领会每个调用的含义,直观、简洁。更重要的,是在做到直观简洁的同时,兼具了强大的功能,使得其在复杂的开发情境下依旧保持原有的风格。
Laravel 的 where 方法的参数很多,实际用到的只有前三个参数,WHERE a = x,这个表达式中 “a” 是参数 0,“=” 是参数 1,“b” 是参数 2。在参数 1 为 “=” 时,可以省略,而后直接将参数 2 挪至参数 1 的位置。无论如何,都很直观。
复杂的条件逻辑的实现也很简单,Laravel 还提供了另外几种 where 方法:orWhere、(or)whereIn、(or)whereNotNull、(or)whereNull、(or)whereBetween。看过一次的人基本无需翻阅文档两次。更为重要的,Laravel 可以很轻松地实现 WHERE (a = x AND b =y) OR c = z 这种形式,并且依旧优雅和富有表现力,不再赘述,前文已经体现过了 Laravel 利用匿名函数实现这种括号包裹和子查询的功能。
我们来看看 Laravel 是如何书写它的 where 代码的:
public function where ( $column , $operator = null , $value = null , $boolean = 'and' ) { if ( is_array ( $column )) { return $this -> addArrayOfWheres ( $column , $boolean ); } if ( func_num_args () == 2 ) { list ( $value , $operator ) = [ $operator , '=' ]; } elseif ( $this -> invalidOperatorAndValue ( $operator , $value )) { throw new InvalidArgumentException ( 'Illegal operator and value combination.' ); } if ( $column instanceof Closure ) { return $this -> whereNested ( $column , $boolean ); } if (! in_array ( strtolower ( $operator ), $this -> operators , true ) && ! in_array ( strtolower ( $operator ), $this -> grammar -> getOperators (), true )) { list ( $value , $operator ) = [ $operator , '=' ]; } if ( $value instanceof Closure ) { return $this -> whereSub ( $column , $operator , $value , $boolean ); } if ( is_null ( $value )) { return $this -> whereNull ( $column , $boolean , $operator != '=' ); } $type = 'Basic' ; if ( Str :: contains ( $column , '->' ) && is_bool ( $value )) { $value = new Expression ( $value ? 'true' : 'false' ); } $this -> wheres [] = compact ( 'type' , 'column' , 'operator' , 'value' , 'boolean' ); if (! $value instanceof Expression ) { $this -> addBinding ( $value , 'where' ); } return $this ; }
上述代码中有着对另外部分方法的调用,但并不影响我们看出一些端倪。不过 Laravel 的东西拆的太细,如果要将其调用的方法全部展示篇幅会很大,我将会着重分析几个点。
我们注意到这个 where 方法其中有些和其他框架组件不太一致的设计:
多余的参数是干什么的? 为什么要拆分的那么细? Expression 这个类是干嘛的?
这也是这部分的重点。关于这部分,下一篇再讲。
关于查询构造器,我们后几篇文章中除了分析这个 where 的代码,还有包括一些承接性质的方法比如 addBinding 这些不是很起眼的但却非常重要的方法,当然也会引入语法生成器的介绍,谢谢各位的关注!
感谢博主:https://www.insp.top/article/learn-laravel-query-builder-part-1
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】 进行投诉反馈!