标题:【Laravel系列4.2】查询构造器
查询构造器
什么是查询构造器?其实就像我们上篇文章中学习过的使用原始 SQL 语句的方式来操作数据库一样,查询构造器这个东西就是在这个原始操作的基础上为我们封装了一系列的接口,能够让我们方便地来操作数据库。或者说,就是像我们很早前自己封装的那种 MySQL 类一样,框架帮我们完成了这一步。并且,最主要的是,它可以让我们以链式调用的形式来操作数据库,从而避免去写繁杂混乱的 SQL 语句。先卖个关子,想想这和哪个设计模式有关?(文中自会揭晓)
今天的测试表和上篇文章的一样,改下表名或者直接用上篇文章的都可以。
增删改查
话不多说,马上我们就进入最简单的增删改查的操作学习。
Route::get('db/test/insert', function () {
$data = [
[
'name'=>'Peter',
'sex' => 1,
],
[
'name'=>'Tom',
'sex' => 1,
],
[
'name'=>'Susan',
'sex' => 2,
],
[
'name'=>'Mary',
'sex' => 2,
],
[
'name'=>'Jim',
'sex' => 1,
],
];
foreach ($data as $v) {
$insertId = \Illuminate\Support\Facades\DB::table('db_test')->insertGetId($v);
echo $insertId, '<br/>';
}
});
Route::get('db/test/update', function () {
$data = [
'name' => request()->name,
'sex' => request()->sex,
'id' => request()->id
];
if($data['id'] < 1 || !$data['name'] || !in_array($data['sex'], [1, 2])){
echo '参数错误';
}
\Illuminate\Support\Facades\DB::table('db_test')->where('id', '=', $data['id'])->update($data);
echo '修改成功';
});
Route::get('db/test/delete', function () {
$id = request()->id;
if($id < 1){
echo '参数错误';
}
\Illuminate\Support\Facades\DB::table('db_test')->delete($id);
echo '删除成功';
});
Route::get('db/test/list', function () {
$where = [];
if(request()->name){
$where[] = ['name', 'like', '%' . request()->name . '%'];
}
if(request()->sex){
$where[] = ['sex', '=', request()->sex];
}
$list = \Illuminate\Support\Facades\DB::table('db_test')
->select(['*'])
->where($where)
->orderBy('id', 'desc')
->limit(10)
->offset(0)
->get()
->toArray();
dd($list);
});
Route::get('db/test/info', function () {
$id = (int)request()->get('id', 0);
$info = \Illuminate\Support\Facades\DB::table('db_test')->find($id);
dd($info);
});
一次性,我们就把增删改查的代码全部放上来了。其实看到这种写法,不知道学过 Java 和 .NET 的小伙伴会不会感觉非常熟悉。在 Java 中,最早的 Hibernate ,在 .NET 中的 Linq 都有这种写法。通过链式调用,来构造 SQL 语句进行数据库的操作。注意,这里还不是完全的 面向对象 的写法。我们下篇文章要讲到的模型才是真正的面向对象的写法。其实,查询构造器就相当于我们将原始 SQL 的操作进行了一次封装而已。而且,在模型中,其实内部调用的也是这个 查询构造器 。也就是说,查询构造器是介于 模型 和 原始语句 操作中间的一层。
不过相对来说,模型需要每个表都建立,而且表间关系复杂的话 Model 类也会比较复杂,而查询构造器会更简单而且更方便使用。当然,要使用哪种一般会是团队的选择,而且往往更多情况下是 查询构造器 和 模型 两个结合起来使用。
好了,话说回来,我们还是看看代码。使用 查询构造器 也是通过一个 DB 门面,但是,在这里我们需要通过 table() 方法指定一个表名。之后的操作就全都是针对这个指定的表名了。接下来,我们就可以通过链式调用的方式进行数据库的操作。先来看简单的增删改。
使用 insertGetId() 我们可以插入一条数据并返回这条数据的主键 ID ,这个相信会是大家最常用的。当然,也有 insert() 方法,它返回的是成功失败。另外,像上面测试代码中我们是一条一条地插入数据的,也可以整个批量地插入数据,后面我们会讲到。update() 方法是用于更新的,它返回的是受影响的条数,这个方法需要有一个 where() 函数用于提供更新数据的条件,如果不带 where() 的话也是可以的,不过后果自己承担哈。delete() 方法用于删除数据,它可以直接指定一个数据的主键 ID ,同时它也可以使用 where() 条件的方式删除,大家可以自己尝试一下。
查询语句相对来说会复杂一些,我们在测试代码中增加了 where() 、orderBy() 和分页相关的组织函数。最后,通过一个 get() 函数就可以获得列表的信息。大家可以看到,在代码中我还使用了一个 toArray() 结尾,这样返回的就是一个数组。如果在没有做其它设置的情况下,这个数组里的每一项会是一个 stdClass 对象。还记得 PDO 中 FETCH_MODE 相关的配置吗?如果不记得的小伙伴可以回去复习一下 【PHP中的PDO操作学习(一)初始化PDO及原始SQL语句操作】https://mp.weixin.qq.com/s/Lh4jiaLA64lVwRZCAmq_OQ 。在 Laravel 中,默认情况下这个值设置的就是 PDO::FETCH_OBJ 。关于如何修改成 PDO::FETCH_ASSOC ,我们会在后面的文章中学习。
最后,我们还有一个获取单个数据的方法 find() ,它和 delete() 很类似,只需要一个主键 ID 就可以了。
在 查询构造器 中,还有其它很多的链式函数可以实现非常复杂的数据库操作,大家可以自己去研究一下。
在这里还需要注意的是,链式调用每个函数方法的返回值哦,只有返回的是 Builder 对象的才可以不停地链式哈,get()、toArray()、find() 之后可不能再继续链式了,因为它们返回的是结果对象、数组或者是一个 stdClass 了,已经不是可以持续构造的 Builder 对象了。细心的小伙伴一定会发现这个 Builder 是不是有点眼熟?赶紧去 【PHP设计模式之建造者模式】https://mp.weixin.qq.com/s/AhCLhH3rQAOULdZ2NtSGDw 中复习一下吧,构造器 建造者 这两个名词是可以互换的哦,这下明白为什么今天我们这篇文章和这些功能为什么叫做 查询构造器 了吧。我们又发现了一个设计模式在 Laravel 框架中的应用,意外不意外,惊喜不惊喜!
连表查询
普通的连表查询的使用还是非常简单的,我也就不多说了,下面的代码中也有演示。一般的连表查询,我们只需要一个外键相对应即可,但是在我的实际业务开发中,还会有遇到多个键相对应的情况,这个才是我们接下来说的重点问题。
Route::get('db/test/join', function () {
// 普通一个外键对应
\Illuminate\Support\Facades\DB::table('db_test', 't')->leftJoin('db_sex as s', 't.sex', '=', 's.id')->dump();
// "select * from `db_test` as `t` left join `db_sex` as `s` on `t`.`sex` = `s`.`id`"
// 多个外键对应
\Illuminate\Support\Facades\DB::table('db_test', 't')->leftJoin('raw_test as rt', function($j){
$j->on('t.name', '=', 'rt.name')->on('t.sex', '=', 'rt.sex');
// $j->on('t.name', '=', 'rt.name')->orOn('t.sex', '=', 'rt.sex')->where('t.id', '<', 10);
})->dump();
// select * from `db_test` as `t` left join `raw_test` as `rt` on `t`.`name` = `rt`.`name` and `t`.`sex` = `rt`.`sex`
});
代码中第一段的连表查询就是最普通的一个外键的查询,如果要实现多个外键连表的话,就需要使用第二种方法。它是 join() 或者 leftJoin() 这些 join 相关的函数都支持的一种形式,把第二个参数变成一个回调参数,然后在里面继续使用 on() 方法来进行多个外键条件的连接。最后输出的 SQL 语句中,join 后面就会有多个条件。
注意看我注释掉的第二种多条件的写法,在这里面我使用了 orOn() 和 where() ,大家可以打开测试一下,结果是如下的 SQL 语句。
// select * from `db_test` as `t` left join `raw_test` as `rt` on `t`.`name` = `rt`.`name` or `t`.`sex` = `rt`.`sex` and `id` < ?
// array:1 [
// 0 => 10
// ]
感觉很复杂吧,日常开发中我们很少会写这样的复杂的连查语句,这里只是让大家知道这些功能要实现都不是问题,如果真的有需要的场景,能想起来可以这么用就行了。
批量插入
前面有提到过,我们可以使用 insert() 进行批量的插入,这里也直接给演示一下,其实没什么特别的东西。不过需要注意的是,insert() 返回的是布尔值,表示成功失败,所以在批量插入的时候想要得到所有的插入 ID 就需要用别的方法了。(比如记录插入前最后一条的 ID 值然后再查询一次大于这个 ID 的所有数据的 ID 值)
Route::get('db/test/batch/insert', function () {
$data = [
[
'name'=>'Peter',
'sex' => 1,
],
[
'name'=>'Tom',
'sex' => 1,
],
[
'name'=>'Susan',
'sex' => 2,
],
[
'name'=>'Mary',
'sex' => 2,
],
[
'name'=>'Jim',
'sex' => 1,
],
];
dd(\Illuminate\Support\Facades\DB::table('db_test')->insert($data));
});
调试
如果想知道最后执行的 SQL 语句是什么,直接使用一个 toSql() 方法就可以了。
echo \Illuminate\Support\Facades\DB::table('db_test')
->select(['*'])
->where($where)
->orderBy('id', 'desc')
->limit(10)
->offset(0)
->toSql();
// select * from `db_test` where (`name` like ?) order by `id` desc limit 10 offset 0
另外还有就是我们在 SQL 语句中只看得到原始语句,也就是 name 这个 where 条件是使用 ? 号占位符的,参数是没法通过 toSql() 看到的。但是我们还是很想知道我们的参数是什么呀,从而方便我们的调试,这可怎么办呢。不用担心,还有好东西呢。
\Illuminate\Support\Facades\DB::table('db_test')
->select(['*'])
->where($where)
->orderBy('id', 'desc')
->limit(10)
->offset(0)
->dd();
// "select * from `db_test` where (`name` like ?) order by `id` desc limit 10 offset 0"
// array:1 [▼
// 0 => "%m%"
// ]
dd() 这个方法会输出两行信息,一行是 SQL 语句,一行就是条件参数数组,是不是非常方便。不过它会中断程序的运行,我们还有另一个方法 dump() ,输出的内容是和这个 dd() 方法完全相同的,但它不会中断程序的运行。
有这三大神器,相信你对 查询构造器 的调试就能够得心应手了吧!
底层真的是调用的原始操作方法?
我们选用最简单的 update() 方法看一下,因为它的代码实在是太明显了。直接通过编辑器的跳转功能点击 update() 方法就会跳转到 laravel/framework/src/Illuminate/Database/Query/Builder.php 的 update() 方法中。
public function update(array $values)
{
$sql = $this->grammar->compileUpdate($this, $values);
return $this->connection->update($sql, $this->cleanBindings(
$this->grammar->prepareBindingsForUpdate($this->bindings, $values)
));
}
额,有点太直接了吧,$this->connection->update() 是啥?就是我们上篇文章中学习过的 DB::connection('xxx')->update() 好不好!compileUpdate() 很明显地是在组织 SQL 语句,大家也可以直接过去看看,它在 laravel/framework/src/Illuminate/Database/Query/Grammars/Grammar.php 文件中。这个方法中的每个方法里面都是在拼接我们需要的这条 update 更新语句。
public function compileUpdate(Builder $query, array $values)
{
$table = $this->wrapTable($query->from);
$columns = $this->compileUpdateColumns($query, $values);
$where = $this->compileWheres($query);
return trim(
isset($query->joins)
? $this->compileUpdateWithJoins($query, $table, $columns, $where)
: $this->compileUpdateWithoutJoins($query, $table, $columns, $where)
);
}
注意看最后 return 时返回的那两个方法。
protected function compileUpdateWithoutJoins(Builder $query, $table, $columns, $where)
{
return "update {$table} set {$columns} {$where}";
}
protected function compileUpdateWithJoins(Builder $query, $table, $columns, $where)
{
$joins = $this->compileJoins($query, $query->joins);
return "update {$table} {$joins} set {$columns} {$where}";
}
相信我已经不需要再多解释什么了吧。
建造者模式在哪里?
这个就要一步一步来看了,前面其实我们已经看到了 laravel/framework/src/Illuminate/Database/Query/Builder.php 这个对象的类文件,那么我们是怎么通过 DB 门面创建它的呢?
首先就是 DB 门面会生成一个 laravel/framework/src/Illuminate/Database/DatabaseManager.php 对象,在它的内部,如果我们没有指定 connection() 的话,它也会创建一个默认的 connection() 对象,就是我们上篇文章中演示的连接不同数据的效果。然后这个 connection() 会通过上篇文章讲过的工厂方法创建一个 MySqlConnector 对象,它会继续创建 laravel/framework/src/Illuminate/Database/MySqlConnection.php 连接对象。这个对象继承的 laravel/framework/src/Illuminate/Database/Connection.php 类中,就有一个 table() 方法。
// laravel/framework/src/Illuminate/Database/Connection.php
public function table($table, $as = null)
{
return $this->query()->from($table, $as);
}
这个方法继续调用 query() 方法,实际就是创建了一个建造者对象。
use Illuminate\Database\Query\Builder as QueryBuilder;
// laravel/framework/src/Illuminate/Database/Connection.php
public function query()
{
return new QueryBuilder(
$this, $this->getQueryGrammar(), $this->getPostProcessor()
);
}
注意这个 QueryBuilder 实际上是 use Illuminate\Database\Query\Builder 的别名,就是我们一直看到的那个建造者类。继续看建造者类中的 from() 方法。
// laravel/framework/src/Illuminate/Database/Query/Builder.php
public function from($table, $as = null)
{
if ($this->isQueryable($table)) {
return $this->fromSub($table, $as);
}
$this->from = $as ? "{$table} as {$as}" : $table;
return $this;
}
看到没有,已经开始在构建原始的 SQL 语句了。注意到它返回的是 $this ,这个嘛,还是那句话,找前面的链接去看下建造者模式是如何实现的,特别是那篇文章中最下面的那个例子。
好了,你可以继续查看这个类中的其它方法,可以发现 where() 、join() 这类的方法返回的都是 $this ,通过这种返回 自身对象 的方式就可以继续链式调用,通过它们,我们就可以不断的为这个类中相对应的属性添加内容,是不是和我们在建造者模式中的例子非常类似。只不过我们在那篇文章中没有使用这种返回 $this 的操作而已。
总结
关于 查询构造器 的其它使用在官方文档上都有,今天的文章就只是简单地介绍了一些常用的和独特的查询构造方式而已,毕竟我们的系列文章的主旨还是在分析源码上。这篇文章中,我们又看到了 建造者模式 的应用,以及了解到了 链式调用 是如何实现的。而且更重要的是,我们也确认了 查询构造器 确实在底层还是使用的 原始SQL 的方式执行的。同时,我们也找到了构造器创建的地方。依然是收获满满的一天呀。接下来,我们更进一层,下篇文章将看看如何通过 ORM 映射的 Model 来实现数据库操作的,并且看看它们是如何运行的。
参考文档:
https://learnku.com/docs/laravel/8.x/queries/9401