跳到主要內容

查詢建構器

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

資料庫:查詢建構器

簡介

Laravel 的資料庫查詢建構器提供了一個方便、流暢的介面來建立和運行資料庫查詢。它可用於執行應用程式中的大多數資料庫操作,並且與 Laravel 支援的所有資料庫系統完美配合。

Laravel 查詢建構器使用 PDO 參數綁定來保護您的應用程式免受 SQL 注入攻擊。無需清理或清理作為查詢綁定傳遞給查詢建構器的字串。

[!WARNING] PDO 不支援綁定欄位名稱。因此,您不應允許使用者輸入來決定查詢引用的欄位名稱,包括「order by」欄位。

執行資料庫查詢

從表中檢索所有行

您可以使用 DB Facade 提供的 table 方法來開始查詢。table 方法為給定的表返回一個流暢的查詢建構器實例,允許您向查詢添加更多約束,然後最終使用 get 方法檢索查詢的結果:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * 顯示應用程式所有使用者的列表。
     */
    public function index(): View
    {
        $users = DB::table('users')->get();

        return view('user.index', ['users' => $users]);
    }
}

get 方法返回一個 Illuminate\Support\Collection 實例,其中包含查詢的結果,每個結果都是 PHP stdClass 物件的實例。您可以透過將欄位作為物件的屬性來存取每個欄位的值:

use Illuminate\Support\Facades\DB;

$users = DB::table('users')->get();

foreach ($users as $user) {
    echo $user->name;
}

[!NOTE] Laravel 集合提供了多種極其強大的方法來映射和歸約資料。有關 Laravel 集合的更多資訊,請查看集合文檔

從表中檢索單行/欄位

如果您只需要從資料庫表中檢索一行,您可以使用 DB Facade 的 first 方法。此方法將返回一個單一的 stdClass 物件:

$user = DB::table('users')->where('name', 'John')->first();

return $user->email;

如果您想從資料庫表中檢索一行,但如果未找到匹配的行則拋出 Illuminate\Database\RecordNotFoundException,您可以使用 firstOrFail 方法。如果未捕獲 RecordNotFoundException,將自動向客戶端發送 404 HTTP 響應:

$user = DB::table('users')->where('name', 'John')->firstOrFail();

如果您不需要整行,可以使用 value 方法從記錄中提取單個值。此方法將直接返回欄位的值:

$email = DB::table('users')->where('name', 'John')->value('email');

要透過其 id 欄位值檢索一行,請使用 find 方法:

$user = DB::table('users')->find(3);

檢索欄位值列表

如果您想檢索一個包含單個欄位值的 Illuminate\Support\Collection 實例,您可以使用 pluck 方法。在此範例中,我們將檢索使用者頭銜的集合:

use Illuminate\Support\Facades\DB;

$titles = DB::table('users')->pluck('title');

foreach ($titles as $title) {
    echo $title;
}

您可以透過向 pluck 方法提供第二個參數來指定結果集合應使用的欄位作為其金鑰:

$titles = DB::table('users')->pluck('title', 'name');

foreach ($titles as $name => $title) {
    echo $title;
}

分塊結果

如果您需要處理數千條資料庫記錄,請考慮使用 DB Facade 提供的 chunk 方法。此方法一次檢索一小部分結果,並將每個區塊傳遞給一個閉包進行處理。例如,讓我們一次以 100 條記錄為一個區塊檢索整個 users 表:

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    foreach ($users as $user) {
        // ...
    }
});

您可以透過從閉包返回 false 來停止進一步的區塊被處理:

DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    // 處理記錄...

    return false;
});

如果您在分塊結果時更新資料庫記錄,您的區塊結果可能會以意外的方式發生變化。如果您計劃在分塊時更新檢索到的記錄,最好使用 chunkById 方法代替。此方法將根據記錄的主鍵自動分頁結果:

DB::table('users')->where('active', false)
    ->chunkById(100, function (Collection $users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update(['active' => true]);
        }
    });

由於 chunkByIdlazyById 方法向正在執行的查詢添加了自己的「where」條件,您通常應該在閉包內邏輯分組您自己的條件:

DB::table('users')->where(function ($query) {
    $query->where('credits', 1)->orWhere('credits', 2);
})->chunkById(100, function (Collection $users) {
    foreach ($users as $user) {
        DB::table('users')
            ->where('id', $user->id)
            ->update(['credits' => 3]);
    }
});

[!WARNING] 在區塊回呼中更新或刪除記錄時,對主鍵或外鍵的任何更改都可能影響區塊查詢。這可能導致記錄未被包含在分塊結果中。

延遲串流結果

lazy 方法與分塊方法的工作方式類似,因為它以區塊形式執行查詢。但是,它不是將每個區塊傳遞給回呼,而是返回一個 LazyCollection,讓您可以將結果作為單個流進行互動:

use Illuminate\Support\Facades\DB;

DB::table('users')->orderBy('id')->lazy()->each(function (object $user) {
    // ...
});

同樣,如果您計劃在迭代檢索到的記錄時更新它們,最好使用 lazyByIdlazyByIdDesc 方法代替。這些方法將根據記錄的主鍵自動分頁結果:

DB::table('users')->where('active', false)
    ->lazyById()->each(function (object $user) {
        DB::table('users')
            ->where('id', $user->id)
            ->update(['active' => true]);
    });

[!WARNING] 在迭代記錄時更新或刪除記錄時,對主鍵或外鍵的任何更改都可能影響區塊查詢。這可能導致記錄未被包含在結果中。

聚合

查詢建構器還提供了多種用於檢索聚合值的方法,如 countmaxminavgsum。您可以在建立查詢後調用這些方法中的任何一個:

use Illuminate\Support\Facades\DB;

$users = DB::table('users')->count();

$price = DB::table('orders')->max('price');

當然,您可以將這些方法與其他子句結合使用,以微調聚合值的計算方式:

$price = DB::table('orders')
    ->where('finalized', 1)
    ->avg('price');

判斷記錄是否存在

不是使用 count 方法來判斷是否存在匹配查詢約束的記錄,您可以使用 existsdoesntExist 方法:

if (DB::table('orders')->where('finalized', 1)->exists()) {
    // ...
}

if (DB::table('orders')->where('finalized', 1)->doesntExist()) {
    // ...
}

SELECT 語句

指定 SELECT 子句

您可能並非總是想從資料庫表中選擇所有欄位。使用 select 方法,您可以為查詢指定自訂的「select」子句:

use Illuminate\Support\Facades\DB;

$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();

原始表達式

有時您可能需要向查詢中插入一個任意字串。要建立一個原始字串表達式,您可以使用 DB Facade 提供的 raw 方法:

$users = DB::table('users')
    ->select(DB::raw('count(*) as user_count, status'))
    ->where('status', '<>', 1)
    ->groupBy('status')
    ->get();

[!WARNING] 原始語句將作為字串注入查詢中,因此您應極度小心以避免建立 SQL 注入漏洞。

原始方法

不是使用 DB::raw 方法,您還可以使用以下方法向查詢的各個部分插入原始表達式。請記住,Laravel 無法保證任何使用原始表達式的查詢都受到 SQL 注入漏洞的保護。

selectRaw

selectRaw 方法可用於替代 addSelect(DB::raw(/* ... */))。此方法接受一個可選的綁定陣列作為其第二個參數:

$orders = DB::table('orders')
    ->selectRaw('price * ? as price_with_tax', [1.0825])
    ->get();

whereRaw / orWhereRaw

whereRaworWhereRaw 方法可用於向查詢中注入原始「where」子句。這些方法接受一個可選的綁定陣列作為其第二個參數:

$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "TX", ?, 100)', [200])
    ->get();

havingRaw / orHavingRaw

havingRaworHavingRaw 方法可用於提供一個原始字串作為「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();

連接

內連接子句

查詢建構器也可用於向查詢添加連接子句。要執行基本的「內連接」,您可以在查詢建構器實例上使用 join 方法。傳遞給 join 方法的第一個參數是您需要連接到的表名,而其餘參數指定連接的欄位約束。您甚至可以在單個查詢中連接多個表:

use Illuminate\Support\Facades\DB;

$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();

左連接/右連接子句

如果您想執行「左連接」或「右連接」而不是「內連接」,請使用 leftJoinrightJoin 方法。這些方法與 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();

交叉連接子句

您可以使用 crossJoin 方法來執行「交叉連接」。交叉連接在第一個表和連接的表之間生成笛卡爾積:

$sizes = DB::table('sizes')
    ->crossJoin('colors')
    ->get();

進階連接子句

您還可以指定更進階的連接子句。首先,將一個閉包作為第二個參數傳遞給 join 方法。閉包將接收一個 Illuminate\Database\Query\JoinClause 實例,允許您指定「連接」子句的約束:

DB::table('users')
    ->join('contacts', function (JoinClause $join) {
        $join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */);
    })
    ->get();

如果您想在連接上使用「where」子句,您可以使用 JoinClause 實例提供的 whereorWhere 方法。這些方法不是比較兩個欄位,而是將欄位與一個值進行比較:

DB::table('users')
    ->join('contacts', function (JoinClause $join) {
        $join->on('users.id', '=', 'contacts.user_id')
            ->where('contacts.user_id', '>', 5);
    })
    ->get();

子查詢連接

您可以使用 joinSubleftJoinSubrightJoinSub 方法將查詢連接到子查詢。這些方法中的每一個都接收三個參數:子查詢、其表別名和一個定義相關欄位的閉包。在此範例中,我們將檢索使用者集合,其中每個使用者記錄還包含使用者最近發布的部落格文章的 created_at 時間戳:

$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 (JoinClause $join) {
        $join->on('users.id', '=', 'latest_posts.user_id');
    })->get();

側向連接

[!WARNING] 側向連接目前僅由 PostgreSQL、MySQL >= 8.0.14 和 SQL Server 支援。

您可以使用 joinLateralleftJoinLateral 方法與子查詢執行「側向連接」。這些方法中的每一個都接收兩個參數:子查詢和其表別名。連接條件應在給定子查詢的 where 子句中指定。側向連接針對每一行進行評估,並且可以引用子查詢外部的欄位。

在此範例中,我們將檢索使用者集合以及使用者最近的三篇部落格文章。每個使用者最多可以在結果集中產生三行:每個最近的部落格文章一行。連接條件在子查詢中使用 whereColumn 子句指定,引用當前使用者行:

$latestPosts = DB::table('posts')
    ->select('id as post_id', 'title as post_title', 'created_at as post_created_at')
    ->whereColumn('user_id', 'users.id')
    ->orderBy('created_at', 'desc')
    ->limit(3);

$users = DB::table('users')
    ->joinLateral($latestPosts, 'latest_posts')
    ->get();

聯合

查詢建構器還提供了一個方便的方法來將兩個或多個查詢「聯合」在一起。例如,您可以建立一個初始查詢並使用 union 方法將其與更多查詢聯合:

use Illuminate\Support\Facades\DB;

$usersWithoutFirstName = DB::table('users')
    ->whereNull('first_name');

$users = DB::table('users')
    ->whereNull('last_name')
    ->union($usersWithoutFirstName)
    ->get();

除了 union 方法之外,查詢建構器還提供了一個 unionAll 方法。使用 unionAll 方法組合的查詢不會移除其重複結果。unionAll 方法與 union 方法具有相同的方法簽名。

基本 WHERE 子句

WHERE 子句

您可以使用查詢建構器的 where 方法向查詢添加「where」子句。對 where 方法最基本的呼叫需要三個參數。第一個參數是欄位的名稱。第二個參數是一個運算符,可以是資料庫支援的任何運算符。第三個參數是要與欄位值進行比較的值。

例如,以下查詢檢索 votes 欄位值等於 100age 欄位值大於 35 的使用者:

$users = DB::table('users')
    ->where('votes', '=', 100)
    ->where('age', '>', 35)
    ->get();

為了方便,如果您想驗證一個欄位是否 = 給定的值,您可以將該值作為第二個參數傳遞給 where 方法。Laravel 將假設您想使用 = 運算符:

$users = DB::table('users')->where('votes', 100)->get();

您還可以向 where 方法傳遞一個關聯式陣列來快速查詢多個欄位:

$users = DB::table('users')->where([
    'first_name' => 'Jane',
    'last_name' => 'Doe',
])->get();

如前所述,您可以使用您的資料庫系統支援的任何運算符:

$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 函數傳遞一個條件陣列。陣列的每個元素都應該是一個包含通常傳遞給 where 方法的三個參數的陣列:

$users = DB::table('users')->where([
    ['status', '=', '1'],
    ['subscribed', '<>', '1'],
])->get();

[!WARNING] PDO 不支援綁定欄位名稱。因此,您不應允許使用者輸入來決定查詢引用的欄位名稱,包括「order by」欄位。

[!WARNING] MySQL 和 MariaDB 在字串-數字比較中自動將字串類型轉換為整數。在此過程中,非數字字串將被轉換為 0,這可能導致意外結果。例如,如果您的表有一個值為 aaasecret 列,並且您運行 User::where('secret', 0),則將返回該行。為了避免這種情況,請確保在查詢中使用所有值之前將其類型轉換為適當的類型。

OR WHERE 子句

當鏈接查詢建構器的 where 方法呼叫時,「where」子句將使用 and 運算符連接在一起。但是,您可以使用 orWhere 方法使用 or 運算符向查詢添加子句。orWhere 方法接受與 where 方法相同的參數:

$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere('name', 'John')
    ->get();

如果您需要將「or」條件分組在括號內,您可以將一個閉包作為第一個參數傳遞給 orWhere 方法:

use Illuminate\Database\Query\Builder; 

$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere(function (Builder $query) {
        $query->where('name', 'Abigail')
            ->where('votes', '>', 50);
        })
    ->get();

上面的範例將產生以下 SQL:

select * from users where votes > 100 or (name = 'Abigail' and votes > 50)

[!WARNING] 您應該始終將 orWhere 呼叫分組,以避免在應用全域作用域時出現意外行為。

WHERE NOT 子句

whereNotorWhereNot 方法可用於否定一組給定的查詢約束。例如,以下查詢排除了清倉中或價格低於十的產品:

$products = DB::table('products')
    ->whereNot(function (Builder $query) {
        $query->where('clearance', true)
            ->orWhere('price', '<', 10);
        })
    ->get();

WHERE ANY / ALL / NONE 子句

有時您可能需要將相同的查詢約束應用於多個欄位。例如,您可能想檢索給定列表中任何欄位都「像」給定值的所有記錄。您可以使用 whereAny 方法來完成此操作:

$users = DB::table('users')
    ->where('active', true)
    ->whereAny([
        'name',
        'email',
        'phone',
    ], 'like', 'Example%')
    ->get();

上面的查詢將產生以下 SQL:

SELECT *
FROM users
WHERE active = true AND (
    name LIKE 'Example%' OR
    email LIKE 'Example%' OR
    phone LIKE 'Example%'
)

同樣地,whereAll 方法可用於檢索所有給定欄位都匹配給定約束的記錄:

$posts = DB::table('posts')
    ->where('published', true)
    ->whereAll([
        'title',
        'content',
    ], 'like', '%Laravel%')
    ->get();

上面的查詢將產生以下 SQL:

SELECT *
FROM posts
WHERE published = true AND (
    title LIKE '%Laravel%' AND
    content LIKE '%Laravel%'
)

whereNone 方法可用於檢索任何給定欄位都不匹配給定約束的記錄:

$albums = DB::table('albums')
    ->where('published', true)
    ->whereNone([
        'title',
        'lyrics',
        'tags',
    ], 'like', '%explicit%')
    ->get();

上面的查詢將產生以下 SQL:

SELECT *
FROM albums
WHERE published = true AND NOT (
    title LIKE '%explicit%' OR
    lyrics LIKE '%explicit%' OR
    tags LIKE '%explicit%'
)

JSON WHERE 子句

Laravel 還支援在支援 JSON 欄位類型的資料庫上查詢 JSON 欄位類型。目前,這包括 MariaDB 10.3+、MySQL 8.0+、PostgreSQL 12.0+、SQL Server 2017+ 和 SQLite 3.39.0+。要查詢 JSON 欄位,請使用 -> 運算符:

$users = DB::table('users')
    ->where('preferences->dining->meal', 'salad')
    ->get();

$users = DB::table('users')
    ->whereIn('preferences->dining->meal', ['pasta', 'salad', 'sandwiches'])
    ->get();

您可以使用 whereJsonContainswhereJsonDoesntContain 方法查詢 JSON 陣列:

$users = DB::table('users')
    ->whereJsonContains('options->languages', 'en')
    ->get();

$users = DB::table('users')
    ->whereJsonDoesntContain('options->languages', 'en')
    ->get();

如果您的應用程式使用 MariaDB、MySQL 或 PostgreSQL 資料庫,您可以向 whereJsonContainswhereJsonDoesntContain 方法傳遞一個值的陣列:

$users = DB::table('users')
    ->whereJsonContains('options->languages', ['en', 'de'])
    ->get();

$users = DB::table('users')
    ->whereJsonDoesntContain('options->languages', ['en', 'de'])
    ->get();

此外,您可以使用 whereJsonContainsKeywhereJsonDoesntContainKey 方法來檢索包含或不包含 JSON 金鑰的結果:

$users = DB::table('users')
    ->whereJsonContainsKey('preferences->dietary_requirements')
    ->get();

$users = DB::table('users')
    ->whereJsonDoesntContainKey('preferences->dietary_requirements')
    ->get();

最後,您可以使用 whereJsonLength 方法根據 JSON 陣列的長度來查詢:

$users = DB::table('users')
    ->whereJsonLength('options->languages', 0)
    ->get();

$users = DB::table('users')
    ->whereJsonLength('options->languages', '>', 1)
    ->get();

其他 WHERE 子句

whereLike / orWhereLike / whereNotLike / orWhereNotLike

whereLike 方法允許您向查詢添加「like」子句以進行模式匹配。這些方法提供了一種與資料庫無關的方式來執行字串匹配查詢,並能夠切換大小寫敏感性。預設情況下,字串匹配不區分大小寫:

$users = DB::table('users')
    ->whereLike('name', '%John%')
    ->get();

您可以透過 caseSensitive 參數啟用區分大小寫的搜尋:

$users = DB::table('users')
    ->whereLike('name', '%John%', caseSensitive: true)
    ->get();

orWhereLike 方法允許您添加帶有 like 條件的「or」子句:

$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhereLike('name', '%John%')
    ->get();

whereNotLike 方法允許您向查詢添加「NOT like」子句:

$users = DB::table('users')
    ->whereNotLike('name', '%John%')
    ->get();

同樣地,您可以使用 orWhereNotLike 添加帶有 NOT like 條件的「or」子句:

$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhereNotLike('name', '%John%')
    ->get();

[!WARNING] whereLike 區分大小寫的搜尋選項目前在 SQL Server 上不受支援。

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();

您還可以向 whereIn 方法的第二個參數提供一個查詢物件:

$activeUsers = DB::table('users')->select('id')->where('is_active', 1);

$comments = DB::table('comments')
    ->whereIn('user_id', $activeUsers)
    ->get();

上面的範例將產生以下 SQL:

select * from comments where user_id in (
    select id
    from users
    where is_active = 1
)

[!WARNING] 如果您正在向查詢添加大量整數綁定,可以使用 whereIntegerInRawwhereIntegerNotInRaw 方法來大幅減少記憶體使用量。

whereBetween / orWhereBetween

whereBetween 方法驗證欄位的值是否在兩個值之間:

$users = DB::table('users')
    ->whereBetween('votes', [1, 100])
    ->get();

whereNotBetween / orWhereNotBetween

whereNotBetween 方法驗證欄位的值是否在兩個值之外:

$users = DB::table('users')
    ->whereNotBetween('votes', [1, 100])
    ->get();

whereBetweenColumns / whereNotBetweenColumns / orWhereBetweenColumns / orWhereNotBetweenColumns

whereBetweenColumns 方法驗證欄位的值是否在同一表行的兩個欄位的兩個值之間:

$patients = DB::table('patients')
    ->whereBetweenColumns('weight', ['minimum_allowed_weight', 'maximum_allowed_weight'])
    ->get();

whereNotBetweenColumns 方法驗證欄位的值是否在同一表行的兩個欄位的兩個值之外:

$patients = DB::table('patients')
    ->whereNotBetweenColumns('weight', ['minimum_allowed_weight', 'maximum_allowed_weight'])
    ->get();

whereValueBetween / whereValueNotBetween / orWhereValueBetween / orWhereValueNotBetween

whereValueBetween 方法驗證給定的值是否在同一表行的兩個相同類型欄位的值之間:

$products = DB::table('products')
    ->whereValueBetween(100, ['min_price', 'max_price'])
    ->get();

whereValueNotBetween 方法驗證值是否在同一表行的兩個欄位的值之外:

$products = DB::table('products')
    ->whereValueNotBetween(100, ['min_price', 'max_price'])
    ->get();

whereNull / whereNotNull / orWhereNull / orWhereNotNull

whereNull 方法驗證給定欄位的值是否為 NULL

$users = DB::table('users')
    ->whereNull('updated_at')
    ->get();

whereNotNull 方法驗證欄位的值是否不為 NULL

$users = DB::table('users')
    ->whereNotNull('updated_at')
    ->get();

whereNullSafeEquals / orWhereNullSafeEquals

whereNullSafeEqualsorWhereNullSafeEquals 方法可用於將欄位的值與給定的值進行比較,同時將兩個 NULL 值視為相等:

$lastLoginIp = $request->input('last_login_ip');

$users = DB::table('users')
    ->whereNullSafeEquals('last_login_ip', $lastLoginIp)
    ->get();

whereDate / whereMonth / whereDay / whereYear / whereTime

whereDate 方法可用於將欄位的值與日期進行比較:

$users = DB::table('users')
    ->whereDate('created_at', '2016-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', '2016')
    ->get();

whereTime 方法可用於將欄位的值與特定時間進行比較:

$users = DB::table('users')
    ->whereTime('created_at', '=', '11:20:45')
    ->get();

wherePast / whereFuture / whereToday / whereBeforeToday / whereAfterToday

wherePastwhereFuture 方法可用於判斷欄位的值是過去還是未來:

$invoices = DB::table('invoices')
    ->wherePast('due_at')
    ->get();

$invoices = DB::table('invoices')
    ->whereFuture('due_at')
    ->get();

whereNowOrPastwhereNowOrFuture 方法可用於判斷欄位的值是過去還是未來,包含當前日期和時間:

$invoices = DB::table('invoices')
    ->whereNowOrPast('due_at')
    ->get();

$invoices = DB::table('invoices')
    ->whereNowOrFuture('due_at')
    ->get();

whereTodaywhereBeforeTodaywhereAfterToday 方法可用於分別判斷欄位的值是今天、今天之前還是今天之後:

$invoices = DB::table('invoices')
    ->whereToday('due_at')
    ->get();

$invoices = DB::table('invoices')
    ->whereBeforeToday('due_at')
    ->get();

$invoices = DB::table('invoices')
    ->whereAfterToday('due_at')
    ->get();

同樣地,whereTodayOrBeforewhereTodayOrAfter 方法可用於判斷欄位的值是否在今天之前或今天之後,包含今天的日期:

$invoices = DB::table('invoices')
    ->whereTodayOrBefore('due_at')
    ->get();

$invoices = DB::table('invoices')
    ->whereTodayOrAfter('due_at')
    ->get();

whereColumn / orWhereColumn

whereColumn 方法可用於驗證兩個欄位是否相等:

$users = DB::table('users')
    ->whereColumn('first_name', 'last_name')
    ->get();

您還可以向 whereColumn 方法傳遞一個比較運算符:

$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」子句分組在括號內,以實現查詢所需的邏輯分組。事實上,您通常應該始終將 orWhere 方法的呼叫分組在括號內,以避免意外的查詢行為。為此,您可以向 where 方法傳遞一個閉包:

$users = DB::table('users')
    ->where('name', '=', 'John')
    ->where(function (Builder $query) {
        $query->where('votes', '>', 100)
            ->orWhere('title', '=', 'Admin');
    })
    ->get();

如您所見,向 where 方法傳遞一個閉包指示查詢建構器開始一個約束組。閉包將接收一個查詢建構器實例,您可以使用它來設定應包含在括號組中的約束。上面的範例將產生以下 SQL:

select * from users where name = 'John' and (votes > 100 or title = 'Admin')

[!WARNING] 您應該始終將 orWhere 呼叫分組,以避免在應用全域作用域時出現意外行為。

進階 WHERE 子句

WHERE EXISTS 子句

whereExists 方法允許您編寫「where exists」SQL 子句。whereExists 方法接受一個閉包,該閉包將接收一個查詢建構器實例,允許您定義應放在「exists」子句中的查詢:

$users = DB::table('users')
    ->whereExists(function (Builder $query) {
        $query->select(DB::raw(1))
            ->from('orders')
            ->whereColumn('orders.user_id', 'users.id');
    })
    ->get();

或者,您可以向 whereExists 方法提供一個查詢物件而不是閉包:

$orders = DB::table('orders')
    ->select(DB::raw(1))
    ->whereColumn('orders.user_id', 'users.id');

$users = DB::table('users')
    ->whereExists($orders)
    ->get();

上面的兩個範例都將產生以下 SQL:

select * from users
where exists (
    select 1
    from orders
    where orders.user_id = users.id
)

子查詢 WHERE 子句

有時您可能需要建立一個將子查詢的結果與給定值進行比較的「where」子句。您可以透過向 where 方法傳遞一個閉包和一個值來完成此操作。例如,以下查詢將檢索具有給定類型的近期「會員資格」的所有使用者:

use App\Models\User;
use Illuminate\Database\Query\Builder;

$users = User::where(function (Builder $query) {
    $query->select('type')
        ->from('membership')
        ->whereColumn('membership.user_id', 'users.id')
        ->orderByDesc('membership.start_date')
        ->limit(1);
}, 'Pro')->get();

或者,您可能需要建立一個將欄位與子查詢的結果進行比較的「where」子句。您可以透過向 where 方法傳遞一個欄位、運算符和閉包來完成此操作。例如,以下查詢將檢索金額低於平均值的所有收入記錄:

use App\Models\Income;
use Illuminate\Database\Query\Builder;

$incomes = Income::where('amount', '<', function (Builder $query) {
    $query->selectRaw('avg(i.amount)')->from('incomes as i');
})->get();

全文搜尋 WHERE 子句

[!WARNING] 全文搜尋 WHERE 子句目前僅由 MariaDB、MySQL 和 PostgreSQL 支援。

whereFullTextorWhereFullText 方法可用於向具有全文索引的欄位的查詢添加全文「where」子句。這些方法將由 Laravel 轉換為底層資料庫系統的適當 SQL。例如,將為使用 MariaDB 或 MySQL 的應用程式生成 MATCH AGAINST 子句:

$users = DB::table('users')
    ->whereFullText('bio', 'web developer')
    ->get();

向量相似性子句

[!NOTE] 向量相似性子句目前僅在使用 pgvector 擴充套件的 PostgreSQL 連接上受支援。有關定義向量欄位和索引的資訊,請查閱遷移文檔

whereVectorSimilarTo 方法根據與給定向量的餘弦相似度過濾結果,並按相關性對結果排序。minSimilarity 閾值應為 0.01.0 之間的值,其中 1.0 表示完全相同:

$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
    ->limit(10)
    ->get();

當向量參數給出純字串時,Laravel 將自動使用 Laravel AI SDK 為其生成嵌入:

$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', 'Best wineries in Napa Valley')
    ->limit(10)
    ->get();

預設情況下,whereVectorSimilarTo 還按距離排序結果(最相似的在前)。您可以透過將 order 參數傳遞為 false 來禁用此排序:

$documents = DB::table('documents')
    ->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4, order: false)
    ->orderBy('created_at', 'desc')
    ->limit(10)
    ->get();

如果您需要更多控制,可以單獨使用 selectVectorDistancewhereVectorDistanceLessThanorderByVectorDistance 方法:

$documents = DB::table('documents')
    ->select('*')
    ->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
    ->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
    ->orderByVectorDistance('embedding', $queryEmbedding)
    ->limit(10)
    ->get();

使用 PostgreSQL 時,必須在建立 vector 欄位之前載入 pgvector 擴充套件:

Schema::ensureVectorExtensionExists();

排序、分組、限制和偏移

排序

orderBy 方法

orderBy 方法允許您按給定欄位對查詢結果進行排序。orderBy 方法接受的第一個參數應是您要排序的欄位,而第二個參數確定排序的方向,可以是 ascdesc

$users = DB::table('users')
    ->orderBy('name', 'desc')
    ->get();

要按多個欄位排序,您只需根據需要多次調用 orderBy

$users = DB::table('users')
    ->orderBy('name', 'desc')
    ->orderBy('email', 'asc')
    ->get();

排序方向是可選的,預設為升序。如果您想按降序排序,可以指定 orderBy 方法的第二個參數,或者直接使用 orderByDesc

$users = DB::table('users')
    ->orderByDesc('verified_at')
    ->get();

最後,使用 -> 運算符,可以按 JSON 欄位中的值對結果進行排序:

$corporations = DB::table('corporations')
    ->where('country', 'US')
    ->orderBy('location->state')
    ->get();

latestoldest 方法

latestoldest 方法允許您輕鬆地按日期對結果進行排序。預設情況下,結果將按表的 created_at 欄位排序。或者,您可以傳遞要排序的欄位名稱:

$user = DB::table('users')
    ->latest()
    ->first();

隨機排序

inRandomOrder 方法可用於隨機對查詢結果進行排序。例如,您可以使用此方法獲取隨機使用者:

$randomUser = DB::table('users')
    ->inRandomOrder()
    ->first();

移除現有排序

reorder 方法移除先前應用於查詢的所有「order by」子句:

$query = DB::table('users')->orderBy('name');

$unorderedUsers = $query->reorder()->get();

您可以在調用 reorder 方法時傳遞一個欄位和方向,以移除所有現有的「order by」子句並對查詢應用全新的順序:

$query = DB::table('users')->orderBy('name');

$usersOrderedByEmail = $query->reorder('email', 'desc')->get();

為了方便,您可以使用 reorderDesc 方法按降序重新排序查詢結果:

$query = DB::table('users')->orderBy('name');

$usersOrderedByEmail = $query->reorderDesc('email')->get();

分組

groupByhaving 方法

正如您所預期的,groupByhaving 方法可用於對查詢結果進行分組。having 方法的簽名與 where 方法類似:

$users = DB::table('users')
    ->groupBy('account_id')
    ->having('account_id', '>', 100)
    ->get();

您可以使用 havingBetween 方法來篩選給定範圍內的結果:

$report = DB::table('orders')
    ->selectRaw('count(id) as number_of_orders, customer_id')
    ->groupBy('customer_id')
    ->havingBetween('number_of_orders', [5, 15])
    ->get();

您可以向 groupBy 方法傳遞多個參數以按多個欄位分組:

$users = DB::table('users')
    ->groupBy('first_name', 'status')
    ->having('account_id', '>', 100)
    ->get();

要建立更進階的 having 語句,請參閱 havingRaw 方法。

限制和偏移

您可以使用 limitoffset 方法來限制從查詢返回的結果數量,或跳過查詢中的給定數量的結果:

$users = DB::table('users')
    ->offset(10)
    ->limit(5)
    ->get();

條件子句

有時您可能希望某些查詢子句根據另一個條件應用於查詢。例如,您可能只想在傳入的 HTTP 請求上存在給定的輸入值時才應用 where 語句。您可以使用 when 方法來完成此操作:

$role = $request->input('role');

$users = DB::table('users')
    ->when($role, function (Builder $query, string $role) {
        $query->where('role_id', $role);
    })
    ->get();

when 方法僅在第一個參數為 true 時執行給定的閉包。如果第一個參數為 false,則不會執行閉包。因此,在上面的範例中,只有當 role 欄位存在於傳入的請求中並評估為 true 時,才會調用傳遞給 when 方法的閉包。

您可以將另一個閉包作為第三個參數傳遞給 when 方法。此閉包僅在第一個參數評估為 false 時才會執行。為了說明此功能的使用方式,我們將使用它來配置查詢的預設排序:

$sortByVotes = $request->boolean('sort_by_votes');

$users = DB::table('users')
    ->when($sortByVotes, function (Builder $query, bool $sortByVotes) {
        $query->orderBy('votes');
    }, function (Builder $query) {
        $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],
]);

insertOrIgnore 方法將在向資料庫插入記錄時忽略錯誤。使用此方法時,您應該注意重複記錄錯誤將被忽略,並且根據資料庫引擎的不同,其他類型的錯誤也可能被忽略。例如,insertOrIgnore繞過 MySQL 的嚴格模式

DB::table('users')->insertOrIgnore([
    ['id' => 1, 'email' => '[email protected]'],
    ['id' => 2, 'email' => '[email protected]'],
]);

insertUsing 方法將向表中插入新記錄,同時使用子查詢來確定應插入的資料:

DB::table('pruned_users')->insertUsing([
    'id', 'name', 'email', 'email_verified_at'
], DB::table('users')->select(
    'id', 'name', 'email', 'email_verified_at'
)->where('updated_at', '<=', now()->minus(months: 1)));

自動遞增 ID

如果表具有自動遞增的 id,請使用 insertGetId 方法插入一條記錄,然後檢索 ID:

$id = DB::table('users')->insertGetId(
    ['email' => '[email protected]', 'votes' => 0]
);

[!WARNING] 使用 PostgreSQL 時,insertGetId 方法期望自動遞增的欄位名為 id。如果您想從不同的「序列」中檢索 ID,可以將欄位名稱作為第二個參數傳遞給 insertGetId 方法。

Upserts

upsert 方法將插入不存在的記錄,並使用您可以指定的新值更新已存在的記錄。該方法的第一個參數由要插入或更新的值組成,而第二個參數列出在關聯表中唯一識別記錄的列。該方法的第三個也是最後一個參數是一個列的陣列,如果資料庫中已存在匹配的記錄,則應更新這些列:

DB::table('flights')->upsert(
    [
        ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
        ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
    ],
    ['departure', 'destination'],
    ['price']
);

在上面的範例中,Laravel 將嘗試插入兩條記錄。如果已存在具有相同 departuredestination 欄位值的記錄,Laravel 將更新該記錄的 price 欄位。

[!WARNING] 除 SQL Server 外的所有資料庫都要求 upsert 方法第二個參數中的列具有「主鍵」或「唯一」索引。此外,MariaDB 和 MySQL 資料庫驅動程式會忽略 upsert 方法的第二個參數,並始終使用表的「主鍵」和「唯一」索引来偵測現有記錄。

UPDATE 語句

除了向資料庫插入記錄外,查詢建構器還可以使用 update 方法更新現有記錄。update 方法與 insert 方法一樣,接受一個表示要更新的列的列和值對的陣列。update 方法返回受影響的行數。您可以使用 where 子句約束 update 查詢:

$affected = DB::table('users')
    ->where('id', 1)
    ->update(['votes' => 1]);

更新或插入

有時您可能想要更新資料庫中的現有記錄,或者在不存在匹配記錄時建立它。在這種情況下,可以使用 updateOrInsert 方法。updateOrInsert 方法接受兩個參數:一個用於查找記錄的條件陣列,以及一個表示要更新的列的列和值對的陣列。

updateOrInsert 方法將嘗試使用第一個參數的列和值對來定位匹配的資料庫記錄。如果記錄存在,它將使用第二個參數中的值進行更新。如果找不到記錄,將使用兩個參數的合併屬性插入一條新記錄:

DB::table('users')
    ->updateOrInsert(
        ['email' => '[email protected]', 'name' => 'John'],
        ['votes' => '2']
    );

您可以向 updateOrInsert 方法提供一個閉包,以根據匹配記錄的存在來自訂更新或插入資料庫的屬性:

DB::table('users')->updateOrInsert(
    ['user_id' => $user_id],
    fn ($exists) => $exists ? [
        'name' => $data['name'],
        'email' => $data['email'],
    ] : [
        'name' => $data['name'],
        'email' => $data['email'],
        'marketable' => true,
    ],
);

更新 JSON 列

更新 JSON 列時,您應該使用 -> 語法來更新 JSON 物件中的相應金鑰。此操作在 MariaDB 10.3+、MySQL 5.7+ 和 PostgreSQL 9.5+ 上受支援:

$affected = DB::table('users')
    ->where('id', 1)
    ->update(['options->enabled' => true]);

遞增和遞減

查詢建構器還提供了用於遞增或遞減給定欄位值的便利方法。這兩個方法都至少接受一個參數:要修改的欄位。可以提供第二個參數來指定欄位應遞增或遞減的數量:

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']);

此外,您可以使用 incrementEachdecrementEach 方法一次遞增或遞減多個欄位:

DB::table('users')->incrementEach([
    'votes' => 5,
    'balance' => 100,
]);

DELETE 語句

查詢建構器的 delete 方法可用於從表中刪除記錄。delete 方法返回受影響的行數。您可以在調用 delete 方法之前添加「where」子句來約束 delete 語句:

$deleted = DB::table('users')->delete();

$deleted = DB::table('users')->where('votes', '>', 100)->delete();

悲觀鎖定

查詢建構器還包含一些函數,以幫助您在執行 select 語句時實現「悲觀鎖定」。要執行帶有「共享鎖」的語句,您可以調用 sharedLock 方法。共享鎖防止選定的行在您的事務被提交之前被修改:

DB::table('users')
    ->where('votes', '>', 100)
    ->sharedLock()
    ->get();

或者,您可以使用 lockForUpdate 方法。「for update」鎖防止選定的記錄被修改或被另一個共享鎖選定:

DB::table('users')
    ->where('votes', '>', 100)
    ->lockForUpdate()
    ->get();

雖然不是強制性的,但建議將悲觀鎖包裝在事務中。這確保了檢索到的資料在整個操作完成之前在資料庫中保持不變。以防萬一發生故障,事務將自動回滾任何更改並釋放鎖:

DB::transaction(function () {
    $sender = DB::table('users')
        ->lockForUpdate()
        ->find(1);

    $receiver = DB::table('users')
        ->lockForUpdate()
        ->find(2);

    if ($sender->balance < 100) {
        throw new RuntimeException('餘額太低。');
    }

    DB::table('users')
        ->where('id', $sender->id)
        ->update([
            'balance' => $sender->balance - 100
        ]);

    DB::table('users')
        ->where('id', $receiver->id)
        ->update([
            'balance' => $receiver->balance + 100
        ]);
});

可重用查詢元件

如果您的應用程式中有重複的查詢邏輯,您可以使用查詢建構器的 tappipe 方法將邏輯提取到可重用的物件中。想像您的應用程式中有這兩個不同的查詢:

use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;

$destination = $request->query('destination');

DB::table('flights')
    ->when($destination, function (Builder $query, string $destination) {
        $query->where('destination', $destination);
    })
    ->orderByDesc('price')
    ->get();

// ...

$destination = $request->query('destination');

DB::table('flights')
    ->when($destination, function (Builder $query, string $destination) {
        $query->where('destination', $destination);
    })
    ->where('user', $request->user()->id)
    ->orderBy('destination')
    ->get();

您可能想要將查詢之間共有的目標篩選提取到一個可重用的物件中:

<?php

namespace App\Scopes;

use Illuminate\Database\Query\Builder;

class DestinationFilter
{
    public function __construct(
        private ?string $destination,
    ) {
        //
    }

    public function __invoke(Builder $query): void
    {
        $query->when($this->destination, function (Builder $query) {
            $query->where('destination', $this->destination);
        });
    }
}

然後,您可以使用查詢建構器的 tap 方法將物件的邏輯應用於查詢:

use App\Scopes\DestinationFilter;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;

DB::table('flights')
    ->when($destination, function (Builder $query, string $destination) { // [tl! remove]
        $query->where('destination', $destination); // [tl! remove]
    }) // [tl! remove]
    ->tap(new DestinationFilter($destination)) // [tl! add]
    ->orderByDesc('price')
    ->get();

// ...

DB::table('flights')
    ->when($destination, function (Builder $query, string $destination) { // [tl! remove]
        $query->where('destination', $destination); // [tl! remove]
    }) // [tl! remove]
    ->tap(new DestinationFilter($destination)) // [tl! add]
    ->where('user', $request->user()->id)
    ->orderBy('destination')
    ->get();

查詢管道

tap 方法將始終返回查詢建構器。如果您想提取一個執行查詢並返回另一個值的物件,您可以改用 pipe 方法。

考慮以下查詢物件,其中包含整個應用程式中使用的共享分頁邏輯。與將查詢條件應用於查詢的 DestinationFilter 不同,Paginate 物件執行查詢並返回一個分頁器實例:

<?php

namespace App\Scopes;

use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder;

class Paginate
{
    public function __construct(
        private string $sortBy = 'timestamp',
        private string $sortDirection = 'desc',
        private int $perPage = 25,
    ) {
        //
    }

    public function __invoke(Builder $query): LengthAwarePaginator
    {
        return $query->orderBy($this->sortBy, $this->sortDirection)
            ->paginate($this->perPage, pageName: 'p');
    }
}

使用查詢建構器的 pipe 方法,我們可以利用此物件來應用我們的共享分頁邏輯:

$flights = DB::table('flights')
    ->tap(new DestinationFilter($destination))
    ->pipe(new Paginate);

除錯

您可以在建立查詢時使用 dddump 方法來轉儲當前的查詢綁定和 SQL。dd 方法將顯示除錯資訊然後停止執行請求。dump 方法將顯示除錯資訊但允許請求繼續執行:

DB::table('users')->where('votes', '>', 100)->dd();

DB::table('users')->where('votes', '>', 100)->dump();

dumpRawSqlddRawSql 方法可在查詢上調用以轉儲查詢的 SQL,其中所有參數綁定都已正確替換:

DB::table('users')->where('votes', '>', 100)->dumpRawSql();

DB::table('users')->where('votes', '>', 100)->ddRawSql();