簡介 資料庫查詢構建器提供了一個方便的流介面用於創建和執行資料庫查詢。查詢構建器可以用於執行應用中絕大部分資料庫操作,並且能夠在 Laravel 支持的所有資料庫系統上工作。 註:流介面是一種設計模式,更多關於流介面模式的設計和使用方式,可查看這篇教程:PHP 設計模式系列 —— 流介面模式。 La ...
簡介
資料庫查詢構建器提供了一個方便的流介面用於創建和執行資料庫查詢。查詢構建器可以用於執行應用中絕大部分資料庫操作,並且能夠在 Laravel 支持的所有資料庫系統上工作。
註:流介面是一種設計模式,更多關於流介面模式的設計和使用方式,可查看這篇教程:PHP 設計模式系列 —— 流介面模式。
Laravel 查詢構建器使用 PDO 參數綁定來避免 SQL 註入攻擊,不再需要過濾以綁定方式傳遞的字元串。
獲取結果集
從一張表中取出所有行
我們可以從 DB
門面的 table
方法開始,table
方法為給定表返回一個流式查詢構建器實例,該實例允許你在查詢上鏈接多個約束條件並返回最終查詢結果。在本例中,我們使用 get
方法獲取表中所有記錄:
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use Illuminate\Support\Facades\DB; 6 use App\Http\Controllers\Controller; 7 8 class UserController extends Controller 9 { 10 /** 11 * 顯示用戶列表 12 * 13 * @return Response 14 */ 15 public function index() 16 { 17 $users = DB::table('users')->get(); 18 return view('user.index', ['users' => $users]); 19 } 20 }
get
方法返回包含結果集的 Illuminate\Support\Collection
,其中每一個結果都是 PHP 的 StdClass
對象實例:

你可以像訪問對象的屬性一樣訪問欄位的值:
1 foreach ($users as $user) { 2 echo $user->name; 3 }
從一張表中獲取一行/一列
如果你只是想要從數據表中獲取一行數據,可以使用 first
方法,該方法將會返回單個 StdClass
對象:
1 $user = DB::table('users')->where('name', 'John')->first(); 2 echo $user->name;
如果你不需要完整的一行,可以使用 value
方法從結果中獲取單個值,該方法會直接返回指定列的值:
1 $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
方法,你可以為查詢指定自定義的 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)
查詢構建器還可以用於編寫連接語句,關於 SQL 的幾種連接類型,通過下圖可以一目瞭然:

內連接(等值連接)
要實現一個簡單的"內連接",你可以使用查詢構建器實例上的 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
方法。該方法和 join
方法的用法一樣:
$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)
查詢構建器還提供了“聯合”兩個查詢的快捷方式,比如,你可以先創建一個查詢,然後使用 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();
更多 Where 子句
whereBetween
whereBetween
方法驗證列值是否在給定值之間:
$users = DB::table('users') ->whereBetween('votes', [1, 100])->get();
whereNotBetween
whereNotBetween
方法驗證列值不在給定值之間:
$users = DB::table('users') ->whereNotBetween('votes', [1, 100]) ->get();
whereIn/whereNotIn
whereIn
方法驗證給定列的值是否在給定數組中:
$users = DB::table('users') ->whereIn('id', [1, 2, 3]) ->get();
whereNotIn
方法驗證給定列的值不在給定數組中:
$users = DB::table('users') ->whereNotIn('id', [1, 2, 3]) ->get();
whereNull/whereNotNull
whereNull
方法驗證給定列的值為 NULL
:
$users = DB::table('users') ->whereNull('updated_at') ->get();
whereNotNull
方法驗證給定列的值不是 NULL
:
$users = DB::table('users') ->whereNotNull('updated_at') ->get();
whereDate / whereMonth / whereDay / whereYear
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
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', '=', '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')
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 )
JSON Where子句
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
方法允許你通過給定欄位對結果集進行排序,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();
groupBy / having
groupBy
和 having
方法用於對結果集進行分組,having
方法和 where
方法的用法類似:
$users = DB::table('users') ->groupBy('account_id') ->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();
插入(Insert)
查詢構建器還提供了 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
如果數據表有自增ID,使用 insertGetId
方法來插入記錄並返回ID值:
$id = DB::table('users')->insertGetId( ['email' => '[email protected]', 'votes' => 0] );
註:當使用 PostgresSQL 時
insertGetId
方法預設自增列被命名為 id,如果你想要從其他“序列”獲取ID,可以將序列名作為第二個參數傳遞到insertGetId
方法。
更新(Update)
當然,除了插入記錄到資料庫,查詢構建器還可以通過使用 update
方法更新已有記錄。update
方法和 insert
方法一樣,接收欄位名和欄位值的鍵值對數組,對應欄位名就是要更新的列,你可以通過 where
子句來對 update
查詢進行約束:
DB::table('users') ->where('id', 1) ->update(['votes' => 1]);
更新 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' => 'John']);
刪除(Delete)
當然,查詢構建器還可以通過 delete
方法從表中刪除記錄,你可以在調用 delete
方法前通過添加 where
子句來添加約束條件:
DB::table('users')->delete();
DB::table('users')->where('votes', '>', 100)->delete();
如果你希望清除整張表,也就是刪除所有列並將自增 ID 置為 0,可以使用 truncate
方法:
DB::table('users')->truncate();
悲觀鎖 & 樂觀鎖
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖、表鎖、讀鎖、寫鎖等,都是在做操作之前先上鎖。
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像資料庫如果提供類似於 write_condition
機制的其實都是提供的樂觀鎖。
下麵我們看下悲觀鎖和樂觀鎖在 Laravel 中的使用:
悲觀鎖使用
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
都是鎖定性讀)。
這麼說比較抽象,我們舉個計數器的例子:在一條語句中讀取一個值,然後在另一條語句中更新這個值。使用 lock in share mode
的話可以允許兩個事務讀取相同的初始化值,所以執行兩個事務之後最終計數器的值+1;而如果使用 for update
的話,會鎖定第二個事務對記錄值的讀取直到第一個事務執行完成,這樣計數器的最終結果就是+2了。
樂觀鎖使用
樂觀鎖,大多是基於數據版本 ( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 欄位來實現。
讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與資料庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於資料庫表當前版本號,則予以更新,否則認為是過期數據。
總結
兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行重試,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。