数据库查询构建器提供了一个方便的流接口模式用于创建和执行数据库查询。查询构建器可以用于执行应用中绝大部分数据库操作,并且能够在 Laravel 支持的所有数据库系统上工作。
注:流接口模式,也称作方法链,是一种设计模式。
Laravel 查询构建器使用 PDO 参数绑定来避免 SQL 注入攻击,不再需要过滤以绑定方式传递的字符串。
DB 门面的 table 方法为给定表返回一个流式查询构建器实例,该实例允许你在查询上链接多个约束条件并返回最终的查询结果。
在本例中,我们使用 get 方法获取表中的所有记录:
get();
return view('user.index', ['users' => $users]);
}
}
get 方法返回包含结果集的 Illuminate\Support\Collection,其中每一个结果都是 PHP 的 StdClass 对象实例。
可以像访问对象的属性一样访问字段的值:
foreach ($users as $user) {
echo $user->name;
}
如果你只是想要从数据表中获取一行数据,可以使用 first 方法,该方法将会返回单个 StdClass 对象:
$user = DB::table('users')->where('name', 'John')->first();
echo $user->name;
如果你不需要完整的一行,可以使用 value 方法从结果中获取单个值,该方法会直接返回指定列的值:
$email = DB::table('users')->where('name', 'John')->value('email');
如果想要获取包含单个列值的数组,可以使用 pluck 方法。
$titles = DB::table('roles')->pluck('title');
foreach ($titles as $title) {
echo $title;
}
还可以在返回数组中为列值指定自定义键(该自定义键必须是该表的其它字段名,否则会报错):
$roles = DB::table('roles')->pluck('title', 'name');
foreach ($roles as $name => $title) {
echo $title;
}
如果你需要处理成千上百条数据库记录,可以考虑使用 chunk 方法,该方法一次获取结果集的一小块,然后传递每一小块数据到闭包函数进行处理,该方法在编写处理大量数据库记录的 Artisan 命令的时候非常有用。
例如,我们可以将处理全部 users 表数据分割成一次处理 100 条记录的小组块:
DB::table('users')->orderBy('id')->chunk(100, function($users) {
foreach ($users as $user) {
//
}
});
可以通过从闭包函数中返回 false 来终止组块的运行:
DB::table('users')->orderBy('id')->chunk(100, function($users) {
// 处理结果集...
return false;
});
查询构建器还提供了多个聚合方法,如count, max, min, avg 和 sum,你可以在构造查询之后调用这些方法。
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');
当然,你可以联合其它查询子句和聚合函数来构建查询:
$price = DB::table('orders')
->where('finalized', 1)
->avg('price');
我们并不总是想要获取数据表的所有列,使用 select 方法,你可以为查询指定自定义的 select 子句,从而只查询某些字段。
$users = DB::table('users')->select('name', 'email as user_email')->get();
distinct 方法允许你强制查询返回不重复的结果集:
$users = DB::table('users')->distinct()->get();
如果你已经有了一个查询构建器实例并且希望添加一个查询列到已存在的 select 子句,可以使用 addSelect 方法:
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();
有时候你希望在查询中使用原生表达式,这些表达式将会以字符串的形式注入到查询中,所以要格外小心避免 SQL 注入。
想要创建一个原生表达式,可以使用 DB::raw 方法:
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
除了使用 DB::raw 外,你还可以使用以下方法来插入原生表达式到查询的不同部分。
selectRaw
selectRaw 方法可用于替代 select(DB::raw(...)),该方法接收一个可选的绑定数组作为第二个参数:
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();
whereRaw / orWhereRaw
whereRaw 和 orWhereRaw 方法可用于注入原生 where 子句到查询,这两个方法接收一个可选的绑定数组作为第二个参数:
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();
havingRaw / orHavingRaw
havingRaw 和 orHavingRaw 方法可用于设置原生字符串作为 having 子句的值:
$orders = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > 2500')
->get();
orderByRaw
orderByRaw 方法可用于设置原生字符串作为 order by 子句的值:
$orders = DB::table('orders')
->orderByRaw('updated_at - created_at DESC')
->get();
查询构建器还可以用于编写连接语句。
要实现一个简单的”内连接”,你可以使用查询构建器实例上的 join 方法,传递给 join 方法的第一个参数是你需要连接到的表名,剩余的其它参数则是为连接指定的列约束。
$users = DB::table('users')
->join('contacts', 'users.id', '=', 'contacts.user_id')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.*', 'contacts.phone', 'orders.price')
->get();
如果你想要执行左连接,可以使用 leftJoin 方法。
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
要执行交叉连接可以使用 crossJoin 方法,传递你想要交叉连接的表名到该方法即可。交叉连接在第一张表和被连接表之间生成一个笛卡尔积。
$users = DB::table('sizes')
->crossJoin('colours')
->get();
可以指定更多的高级连接子句,传递一个闭包到 join 方法作为第二个参数,该闭包将会接收一个 JoinClause 对象用于指定 join 子句约束。
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(...);
})
->get();
如果你想要在连接中使用“where”风格的子句,可以在查询中使用 where 和orWhere 方法。这些方法会将列和值进行比较而不是列和列进行比较:
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();
查询构建器还提供了“联合”两个查询的快捷方式,比如,你可以先创建一个查询,然后使用 union 方法将其和第二个查询进行联合。
$first = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($first)
->get();
注:unionAll 方法也是有效的,并且和 union 使用方式相同。
使用查询构建器上的 where 方法可以添加 where 子句到查询中。
调用 where 最基本的方式需要传递三个参数,第一个参数是列名,第二个参数是任意一个数据库系统支持的操作符,第三个参数是该列要比较的值。
例如,下面是一个验证“votes”列的值是否等于 100 的查询:
$users = DB::table('users')->where('votes', '=', 100)->get();
为了方便,如果你只是简单比较列值和给定的值是否相等,可以将数值直接作为where 方法的第二个参数:
$users = DB::table('users')->where('votes', 100)->get();
当然,你还可以使用其它操作符来编写 where 子句:
$users = DB::table('users')
->where('votes', '>=', 100)
->get();
$users = DB::table('users')
->where('votes', '<>', 100)
->get();
$users = DB::table('users')
->where('name', 'like', 'T%')
->get();
还可以传递条件数组到 where 方法。
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();
你可以通过方法链将多个 where 约束链接到一起,也可以添加 or 子句到查询。
orWhere 方法和 where 方法接收的参数一样:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();
whereBetween 方法验证列值是否在给定值之间:
$users = DB::table('users')
->whereBetween('votes', [1, 100])->get();
whereNotBetween 方法验证列值不在给定值之间:
$users = DB::table('users')
->whereNotBetween('votes', [1, 100])
->get();
whereIn 方法判断列值在给定数组中:
$users = DB::table('users')
->whereIn('id', [1, 2, 3])
->get();
whereNotIn 方法判断列值不在给定数组中:
$users = DB::table('users')
->whereNotIn('id', [1, 2, 3])
->get();
whereNull 方法判断列值为 NULL:
$users = DB::table('users')
->whereNull('updated_at')
->get();
whereNotNull 方法判断列值不为 NULL。
whereDate 方法用于比较字段值和日期:
$users = DB::table('users')
->whereDate('created_at', '2016-10-10')
->get();
whereMonth 方法用于比较字段值和一年中的指定月份:
$users = DB::table('users')
->whereMonth('created_at', '10')
->get();
whereDay 方法用于比较字段值和一月中的指定日期:
$users = DB::table('users')
->whereDay('created_at', '10')
->get();
whereYear 方法用于比较字段值和指定年:
$users = DB::table('users')
->whereYear('created_at', '2017')
->get();
whereColumn 方法,可用于验证两个字段的值是否相等:
$users = DB::table('users')
->whereColumn('first_name', 'last_name')
->get();
还可以传递一个比较运算符到该方法:
$users = DB::table('users')
->whereColumn('updated_at', '>', 'created_at')
->get();
还可以传递多条件数组到 whereColumn 方法,这些条件通过 and 操作符进行连接:
$users = DB::table('users')
->whereColumn([
['first_name', '=', 'last_name'],
['updated_at', '>', 'created_at']
])->get();
有时候你需要创建更加高级的 where 子句,比如“where exists”或者嵌套的参数分组。
让我们看一个在括号中进行分组约束的例子:
DB::table('users')
->where('name', '=', 'John')
->orWhere(function ($query) {
$query->where('votes', '>', 100)
->where('title', '<>', 'Admin');
})
->get();
传递闭包到 orWhere 方法,查询构建器会开始一个约束分组,该闭包将会获取一个用于设置括号中包含的约束的查询构建器实例。上述语句等价于下面的 SQL:
select * from users where name = 'John' or (votes > 100 and title <> 'Admin')
whereExists 方法允许你编写 where exists SQL 子句,whereExists 方法接收一个闭包参数,该闭包获取一个查询构建器实例从而允许你定义放置在“exists”子句中的查询:
DB::table('users')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
->whereRaw('orders.user_id = users.id');
})
->get();
上述查询等价于下面的 SQL 语句:
select * from users
where exists (
select 1 from orders where orders.user_id = users.id
)
Laravel 还支持在提供 JSON 字段类型的数据库(目前是 MySQL 5.7 和 PostgresSQL)上使用操作符 -> 获取指定 JSON 字段值:
$users = DB::table('users')
->where('options->language', 'en')
->get();
$users = DB::table('users')
->where('preferences->dining->meal', 'salad')
->get();
orderBy 方法允许你通过给定字段对结果集进行排序,orderBy 的第一个参数应该是你希望排序的字段,第二个参数代表排序的方向 asc 或 desc。
$users = DB::table('users')
->orderBy('name', 'desc')
->get();
latest 和 oldest 方法允许你通过日期对结果进行排序,默认情况下,结果集根据 created_at 字段进行排序。你可以按照你想要排序的字段作为字段名传入。
$user = DB::table('users')
->latest()
->first();
inRandomOrder 方法可用于对查询结果集进行随机排序。
比如,你可以用该方法获取一个随机用户:
$randomUser = DB::table('users')
->inRandomOrder()
->first();
groupBy 和 having 方法用于对结果集进行分组和二次筛选,having 方法和 where 方法的用法类似。
$users = DB::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();
skip 方法可以跳过指定数目的记录。
take 方法可以限定查询返回的结果集的数目。
$users = DB::table('users')->skip(10)->take(5)->get();
作为替代方法,还可以使用 offset 和 limit 方法:
$users = DB::table('users')
->offset(10)
->limit(5)
->get();
有时候你可能想要在某些条件为 true 时,才将条件子句应用到查询。
例如,你可能只想给定值在请求中存在的情况下才应用 where 语句,这可以通过 when 方法实现:
$role = $request->input('role');
$users = DB::table('users')
->when($role, function ($query) use ($role) {
return $query->where('role_id', $role);
})
->get();
when 方法只有在第一个参数为 true 的时候才执行给定闭包,如果第一个参数为 false,则闭包不执行。
还可以传递另一个闭包作为 when 方法的第三个参数,该闭包会在第一个参数为 false 的情况下执行。
$sortBy = null;
$users = DB::table('users')
->when($sortBy, function ($query) use ($sortBy) {
return $query->orderBy($sortBy);
}, function ($query) {
return $query->orderBy('name');
})
->get();
查询构建器还提供了 insert 方法用于插入记录到数据表。
insert 方法接收数组形式的字段名和字段值:
DB::table('users')->insert(
['email' => '[email protected]', 'votes' => 0]
);
还可以传入二维数组来插入多条记录:
DB::table('users')->insert([
['email' => '[email protected]', 'votes' => 0],
['email' => '[email protected]', 'votes' => 0]
]);
如果数据表有自增ID,使用 insertGetId 方法来插入记录并返回 ID 值:
$id = DB::table('users')->insertGetId(
['email' => '[email protected]', 'votes' => 0]
);
注:当使用 PostgresSQL 时 insertGetId 方法默认自增列被命名为 id,如果你想要从其他“序列”获取 ID,可以将序列名作为第二个参数传递到 insertGetId 方法。
查询构建器可以通过使用 update 方法更新已有记录。
DB::table('users')
->where('id', 1)
->update(['votes' => 1]);
更新 JSON 字段的时候,需要使用 -> 语法访问 JSON 对象上相应的值,该操作只能用于支持 JSON 字段类型的数据库:
DB::table('users')
->where('id', 1)
->update(['options->enabled' => true]);
查询构建器还为增减给定字段的对应数值提供了方便。
increment() 用于增量更新;decrement() 用于减量更新。
这两个方法都至少接收一个参数:需要修改的列。第二个参数是可选的,用于指定增加或减少的数目。
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);
还支持传入第三个参数,以便同时更新其他的字段。
DB::table('users')->increment('votes', 1, ['name' => 'John']);
查询构建器可以通过 delete 方法从表中删除记录。
DB::table('users')->delete();
DB::table('users')->where('votes', '>', 100)->delete();
如果你希望清空整张表并将自增 ID 置为 0,可以使用 truncate 方法:
DB::table('users')->truncate();
Laravel 查询构建器提供了一些方法帮助你在 select 语句中实现“悲观锁”。
可以在查询中使用 sharedLock 方法,从而在运行语句时带一把”共享锁“。
共享锁可以避免事务处理过程中,选择的行被外界所修改,直到事务处理完毕。
DB::table('users')->where('votes', '>', 100)->sharedLock()->get();
上面这个查询等价于下面这条 SQL 语句:
select * from `users` where `votes` > '100' lock in share mode
还可以使用 lockForUpdate 方法。“for update”锁可以避免选择的行被其它的共享锁修改或删除。
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
上面这个查询等价于下面这条 SQL 语句:
select * from `users` where `votes` > '100' for update
for update 与 lock in share mode 都是用于确保被选中的记录值不能被其它事务更新(上锁)。
两者的区别在于 lock in share mode 不会阻塞其它事务读取被锁定行记录的值,而 for update 会阻塞其他锁定性读对锁定行的读取(非锁定性读仍然可以读取这些记录,lock in share mode 和 for update 都是锁定性读)。
乐观锁: 又称乐观并发控制(Optimistic Concurrency Control),乐观地认为不会发生并发问题,只在提交更新操作时检查是否违反数据的一致性。
乐观锁在数据库中的实现完全是逻辑性的,不需要数据库提供特殊的支持。一般的做法是在数据表中增加一个字段(版本号或者时间戳),作为数据的版本标识。读取数据时,将版本号一同读出;之后更新数据时,加入版本号条件,更新成功就将版本号加1。
乐观锁的重点在于,更新数据时,加入版本号匹配条件,将数据的版本与数据表中对应记录的当前版本进行匹配更新,如果数据的版本号等于数据表的当前版本号,则获取锁成功,也就是更新成功;否则,更新失败,需要回滚整个业务操作。
乐观锁和悲观锁各有优缺点,不可认为一种好于另一种。乐观锁适用于写比较少的情况,即并发冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。