Laravel 查询构建器
简介
数据库查询构建器提供了一个方便的流接口用于创建和执行数据库查询。查询构建器可以用于执行应用中绝大部分数据库操作,并且能够在 Laravel 支持的所有数据库系统上工作。
Laravel 查询构建器使用 PDO 参数绑定来避免 SQL 注入攻击,不再需要过滤以绑定方式传递的字符串。
注:PDO 不支持绑定字段名称,因此,永远不要允许用户输出字段直接被查询语句引用,包括「order by」字段。如果你一定要让用户选择特定的字段进行操作,则必须要验证字段名称是否在白名单中。
获取结果集
从一张表中取出所有行
我们可以从 DB Facade 的 table
方法开始,table
方法为给定表返回一个流式查询构建器实例,该实例允许我们在查询上链接多个约束条件并返回最终查询结果。在本例中,我们使用 get 方法获取表中所有记录:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
/**
* Show a list of all of the application's users.
*
* @return Response
*/
public function index()
{
$users = DB::table('users')->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', '迹忆客')->first();
echo $user->name;
如果我们不需要完整的一行,可以使用 value 方法从结果中获取单个值,该方法会直接返回指定列的值:
$email = DB::table('users')->where('name', '迹忆客')->value('email');
如果我们想要通过 id 字段来获取单条记录,可以使用 find
方法:
$user = DB::table('users')->find(3);
获取字段值列表
如果想要获取包含单个字段值的数组,可以使用 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) {
// Process the records...
return false;
});
如果我们要在组块结果集中更新数据库记录,组块结果可能会以意想不到的方式被更改。因此,在组块查询中更新记录时,最好使用 chunkById 方法。该方法会自动基于记录的主键对结果集进行分页:
DB::table('users')->where('active', false)
->chunkById(100, function ($users) {
foreach ($users as $user) {
DB::table('users')
->where('id', $user->id)
->update(['active' => true]);
}
});
注:在组块回调中更新或删除记录时,对主键或外键的任何更改都会影响组块查询,甚至会导致相应记录不被包含在组块结果集中。
聚合函数
查询构建器还提供了多个聚合方法,如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');
判断记录是否存在
除了通过 count 方法来判断匹配查询条件的结果是否存在外,还可以使用 exists 或 doesntExist 方法:
return DB::table('orders')->where('finalized', 1)->exists();
return DB::table('orders')->where('finalized', 1)->doesntExist();
Select(查询)
指定查询子句
当然,我们并不总是想要获取数据表的所有列,使用 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();
注:原生语句会以字符串的形式注入查询,所以这里尤其要注意避免 SQL 注入攻击。
原生方法
除了使用 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();
groupByRaw
groupByRaw
方法可用于将原始字符串设置为group by子句的值:
$orders = DB::table('orders')
->select('city', 'state')
->groupByRaw('city, state')
->get();
Joins(连表查询)
内连接(inner join)
要实现一个简单的"内连接",我们可以使用查询构建器实例上的 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
或 rightJoin
方法。这些方法和 join
方法的用法一样:
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
$users = DB::table('users')
->rightJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
Cross Join (交叉连接)
要执行“交叉连接”可以使用 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();
子查询连接
我们可以使用 joinSub
、leftJoinSub
和 rightJoinSub
方法将查询和一个子查询进行连接,每个方法都接收三个参数 —— 子查询、表别名和定义关联字段的闭包:
$latestPosts = DB::table('posts')
->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
->where('is_published', true)
->groupBy('user_id');
$users = DB::table('users')
->joinSub($latestPosts, 'latest_posts', function($join) {
$join->on('users.id', '=', 'latest_posts.user_id');
})->get();
联合查询(Union)
查询构建器还提供了“联合”两个查询的快捷方式,比如,可以先创建一个查询,然后使用 union
方法将其和第二个查询进行联合:
$first = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($first)
->get();
注:
unionAll
方法也是有效的,并且和 union 使用方式相同。
Where 子句
简单 Where 子句
使用查询构建器上的 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();
or 语句
我们可以通过方法链将多个 where 约束链接到一起,也可以添加 or 子句到查询,orWhere
方法和 where
方法接收参数一样:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();
如果我们需要对 or 条件进行分组,可以传递一个闭包作为 orWhere 的第一个参数:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere(function($query) {
$query->where('name', 'Abigail')
->where('votes', '>', 50);
})
->get();
// SQL: select * from users where votes > 100 or (name = 'Abigail' and votes > 50)
更多 Where 子句
whereBetween/orWhereBetween
whereBetween
方法验证列值是否在给定值之间:
$users = DB::table('users')
->whereBetween('votes', [1, 100])->get();
whereNotBetween/orWhereNotBetween
whereNotBetween
方法验证列值不在给定值之间:
$users = DB::table('users')
->whereNotBetween('votes', [1, 100])
->get();
whereIn/whereNotIn/orWhereIn/orWhereNotIn
whereIn
方法验证给定列的值是否在给定数组中:
$users = DB::table('users')
->whereIn('id', [1, 2, 3])
->get();
whereNotIn
方法验证给定列的值不在给定数组中:
$users = DB::table('users')
->whereNotIn('id', [1, 2, 3])
->get();
如果要向查询添加大量整数绑定,则可以使用
whereIntegerInRaw
或者whereIntegerNotInRaw
方法来减少内存使用量。
whereNull/whereNotNull/orWhereNull/orWhereNotNull
whereNull
方法验证给定列的值为 NULL:
$users = DB::table('users')
->whereNull('updated_at')
->get();
whereNotNull
方法验证给定列的值不是 NULL:
$users = DB::table('users')
->whereNotNull('updated_at')
->get();
whereDate/whereMonth/whereDay/whereYear/whereTime
whereDate
方法用于比较字段值和日期:
$users = DB::table('users')
->whereDate('created_at', '2019-12-31')
->get();
whereMonth
方法用于比较字段值和一年中的指定月份:
$users = DB::table('users')
->whereMonth('created_at', '12')
->get();
whereDay
方法用于比较字段值和一月中的指定日期:
$users = DB::table('users')
->whereDay('created_at', '31')
->get();
whereYear
方法用于比较字段值和指定年:
$users = DB::table('users')
->whereYear('created_at', '2019')
->get();
whereTime
方法用于比较字段值和指定时间:
$users = DB::table('users')
->whereTime('created_at', '=', '11:20:45')
->get();
whereColumn/orWhereColumn
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”或者嵌套的参数分组。Laravel 查询构建器也可以处理这些。作为开始,让我们看一个在括号中进行分组约束的例子:
DB::table('users')
->where('name', '=', '迹忆客')
->orWhere(function ($query) {
$query->where('votes', '>', 100)
->where('title', '<>', 'Admin');
})
->get();
正如我们所看到的,传递闭包到 orWhere
方法构造查询构建器来开始一个约束分组,该闭包将会获取一个用于设置括号中包含的约束的查询构建器实例。上述语句等价于下面的 SQL:
select * from users where name = '迹忆客' or (votes > 100 and title <> 'Admin')
注:无论何时都需要对 orWhere 调用进行分组以避免应用全局作用域时出现与预期不符 的行为。
where exists 子句
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
)
自查询 Where 子句
有时候我们可能需要构造一个比较子查询结果与给定值的 where 子句,这可以通过传递一个闭包和值到 where 方法来实现。例如,下面的查询会获取所有类型为 Pro 的用户,这个类型是从 membership 表中获取的最新一条记录,如果对应结果 type 值不为 Pro,则返回 null:
use App\User;
$users = User::where(function ($query) {
$query->select('type')
->from('membership')
->whereColumn('user_id', 'users.id')
->orderByDesc('start_date')
->limit(1);
}, 'Pro')->get();
JSON Where 子句
Laravel 还支持在提供 JSON 字段类型的数据库(目前是 MySQL 5.7、 PostgresSQL、SQL Server 2016 以及 SQLite 3.9.0)上使用操作符 -> 获取指定 JSON 字段值:
$users = DB::table('users')
->where('options->language', 'en')
->get();
$users = DB::table('users')
->where('preferences->dining->meal', 'salad')
->get();
可以使用 whereJsonContains
查询 JSON 数组(SQLite 不支持):
$users = DB::table('users')
->whereJsonContains('options->languages', 'en')
->get();
MySQL 和 PostgreSQL 支持在 whereJsonContains 中传入多个值:
$users = DB::table('users')
->whereJsonContains('options->languages', ['en', 'de'])
->get();
可以使用 whereJsonLength
通过长度来查询 JSON 数组:
$users = DB::table('users')
->whereJsonLength('options->languages', 0)
->get();
$users = DB::table('users')
->whereJsonLength('options->languages', '>', 1)
->get();
排序、分组、限定
orderBy
orderBy
方法允许你通过给定字段对结果集进行排序,orderBy 的第一个参数应该是你希望排序的字段,第二个参数控制着排序的方向 —— asc 或 desc:
$users = DB::table('users')
->orderBy('name', 'desc')
->get();
latest / oldest
latest
和 oldest
方法允许你通过日期对结果进行排序,默认情况下,结果集根据 created_at 字段进行排序,或者,你可以按照你想要排序的字段作为字段名传入:
$user = DB::table('users')
->latest()
->first();
inRandomOrder
inRandomOrder
方法可用于对查询结果集进行随机排序,比如,你可以用该方法获取一个随机用户:
$randomUser = DB::table('users')
->inRandomOrder()
->first();
reorder
我们可以使用reorder
方法删除所有现有订单并可选择应用新订单。例如,可以删除所有现有订单:
$query = DB::table('users')->orderBy('name');
$unorderedUsers = $query->reorder()->get();
要删除所有现有订单并应用新订单,请提供 column 和 direction 作为方法的参数:
$query = DB::table('users')->orderBy('name');
$usersOrderedByEmail = $query->reorder('email', 'desc')->get();
groupBy/having
groupBy
和 having
方法用于对结果集进行分组,having
方法和 where
方法的用法类似:
$users = DB::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();
我们可以传递多个参数到 groupBy 方法以便通过多个列进行分组:
$users = DB::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();
关于 having 的更多高级用法,可查看 havingRaw
方法。
skip/take
想要限定查询返回的结果集的数目,或者在查询中跳过给定数目的结果,可以使用skip
和 take
方法:
$users = DB::table('users')->skip(10)->take(5)->get();
作为替代方法,还可以使用 limit
和 offset
方法:
$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();
Inserts (插入)
查询构建器还提供了 insert
方法用于在数据表中插入记录。insert
方法接收数组形式的字段名和字段值进行插入操作:
DB::table('users')->insert(
['email' => 'john@example.com', 'votes' => 0]
);
我们甚至可以一次性通过传入多个数组来插入多条记录,每个数组代表要插入数据表的记录:
DB::table('users')->insert([
['email' => 'taylor@example.com', 'votes' => 0],
['email' => 'dayle@example.com', 'votes' => 0]
]);
insertOrIgnore
方法会在插入记录到数据库时忽略重复记录错误:
DB::table('users')->insertOrIgnore([
['id' => 1, 'email' => 'taylor@example.com'],
['id' => 2, 'email' => 'dayle@example.com']
]);
自增 ID
如果数据表有自增 ID,使用 insertGetId 方法来插入记录并返回ID值:
$id = DB::table('users')->insertGetId(
['email' => 'john@example.com', 'votes' => 0]
);
注:当使用 PostgresSQL 时
insertGetId
方法默认自增列被命名为 id,如果我们的表的自增列的字段名不是“id”,可以在insertGetId方法的第二个参数上指明字段名。
Updates(更新)
当然,除了插入记录到数据库,查询构建器还可以通过使用 update 方法更新已有记录。update 方法和 insert 方法一样,接收字段名和字段值的键值对数组,对应字段名就是要更新的列,你可以通过 where 子句来对 update 查询进行约束:
DB::table('users')
->where('id', 1)
->update(['votes' => 1]);
更新或插入
有时候你可能想要更新数据库中已存在的某条记录,如果对应记录不存在的话,则插入这条记录。在这种场景下,可以使用 updateOrInsert 方法。该方法接收两个参数:用于查询记录的条件数组和用于更新的列值对数组。
updateOrInsert 方法首先会尝试使用第一个参数的列值对匹配对应的数据库记录,如果记录存在,则通过第二个参数来更新它。如果记录不存在,则会合并这两个参数数组然后通过合并后的数组插入一条新纪录:
DB::table('users')
->updateOrInsert(
['email' => 'john@example.com', 'name' => '迹忆客'],
['votes' => '2']
);
更新 JSON 字段
更新 JSON 字段的时候,需要使用 -> 语法访问 JSON 对象上相应的值,该操作只能用于支持 JSON 字段类型的数据库:
DB::table('users')
->where('id', 1)
->update(['options->enabled' => true]);
增加/减少
查询构建器还为增减给定字段名对应数值提供方便。相较于编写 update 语句,这是一条捷径,提供了更好的体验和测试接口。
这两个方法都至少接收一个参数:需要修改的列。第二个参数是可选的,用于控制列值增加/减少的数目。
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' => '迹忆客']);
删除(Delete)
当然,查询构建器还可以通过 delete 方法从表中删除记录,我们可以在调用 delete
方法前通过添加 where 子句来添加约束条件:
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();
此外你还可以使用 lockForUpdate
方法。“for update”锁避免选择行被其它共享锁修改或删除:
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
调试
我们可以在构建查询的时候使用 dd
或 dump
方法来打印查询绑定和 SQL 语句。dd
方法会显示调试信息并停止执行请求,dump
方法也会显示调试信息但允许请求继续执行:
DB::table('users')->where('votes', '>', 100)->dd();
DB::table('users')->where('votes', '>', 100)->dump();