简介

几乎每个现代 Web 应用程序都会与数据库进行交互。Laravel 借助原生 SQL、流式查询构建器 以及 Eloquent ORM,在多种支持的数据库上让数据库交互变得极为简单。目前,Laravel 为以下五种数据库提供官方支持:

此外,MongoDB 可通过由 MongoDB 官方维护的 mongodb/laravel-mongodb 包获得支持。如需了解详细信息,请查阅 Laravel MongoDB 文档。

配置

Laravel 的数据库服务配置位于应用程序的 config/database.php 配置文件中。在此文件中,你可以定义所有数据库连接,并指定默认应使用的连接。此文件中的大多数配置选项都由应用程序的环境变量值驱动。该文件内已为 Laravel 支持的多数数据库系统提供了示例。

默认情况下,Laravel 的示例 环境配置 可直接与 Laravel Sail 配合使用,后者是一种用于在本地机器上开发 Laravel 应用程序的 Docker 配置。不过,你可以根据本地数据库的需要,自由修改数据库配置。

SQLite 配置

SQLite 数据库以单个文件形式存放于文件系统中。你可以在终端中使用 touch 命令创建新的 SQLite 数据库:touch database/database.sqlite。数据库创建完成后,只需将数据库的绝对路径放入 DB_DATABASE 环境变量,即可轻松配置环境变量以指向该数据库:

DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite

默认情况下,SQLite 连接已启用外键约束。如果你想禁用外键约束,应将 DB_FOREIGN_KEYS 环境变量设置为 false:

DB_FOREIGN_KEYS=false

如果你使用 Laravel 安装程序 创建 Laravel 应用程序,并选择 SQLite 作为数据库,Laravel 会自动创建 database/database.sqlite 文件,并为你执行默认的 数据库迁移。

Microsoft SQL Server 配置

若要使用 Microsoft SQL Server 数据库,应确保已安装 sqlsrv 与 pdo_sqlsrv PHP 扩展,以及它们可能依赖的其他组件,例如 Microsoft SQL ODBC 驱动程序。

使用 URL 进行配置

通常,数据库连接通过多个配置值来设置,例如 host、database、username、password 等。每个配置值都有其对应的环境变量。这意味着,在生产服务器上配置数据库连接信息时,你需要管理多个环境变量。

部分托管数据库服务提供商(如 AWS 和 Heroku)会提供一个单一的数据库“URL”,其中以单个字符串包含数据库的全部连接信息。数据库 URL 示例如下:

mysql://root:[email protected]/forge?charset=UTF-8

这些 URL 通常遵循标准结构约定:

driver://username:password@host:port/database?options

为方便起见,Laravel 支持使用这些 URL 作为替代方案,以取代通过多个配置选项来配置数据库的方式。如果存在 url(或对应的 DB_URL 环境变量)配置项,Laravel 将使用它来提取数据库连接与凭据信息。

读写连接

有时你可能希望将 SELECT 语句使用一个数据库连接,而将 INSERT、UPDATE 与 DELETE 语句使用另一个数据库连接。Laravel 让这一过程变得轻而易举,无论你使用的是原生查询、查询构建器还是 Eloquent ORM,系统都会始终选用正确的连接。

要了解读写连接的配置方式,我们先来看以下示例:

'mysql' => [
    'driver' => 'mysql',

    'read' => [
        'host' => [
            '192.168.1.1',
            '196.168.1.2',
        ],
    ],
    'write' => [
        'host' => [
            '192.168.1.3',
        ],
    ],
    'sticky' => true,

    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    'unix_socket' => env('DB_SOCKET', ''),
    'charset' => env('DB_CHARSET', 'utf8mb4'),
    'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
    'prefix' => '',
    'prefix_indexes' => true,
    'strict' => true,
    'engine' => null,
    'options' => extension_loaded('pdo_mysql') ? array_filter([
        (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
    ]) : [],
],

请注意,配置数组中新增了三个键:read、write 和 sticky。read 与 write 键的值为数组,其中包含一个键:host。read 和 write 连接的其余数据库选项将从主 mysql 配置数组中合并而来。

仅当希望覆盖主 mysql 数组中的值时,才需要在 read 与 write 数组中放置条目。因此,在此示例中,192.168.1.1 将作为“读”连接的主机,而 192.168.1.3 将用于“写”连接。主 mysql 数组中的数据库凭据、前缀、字符集及所有其他选项将在两个连接间共享。当 host 配置数组中存在多个值时,每次请求都会随机选择一个数据库主机。

sticky 选项

sticky 选项为可选值,可用于允许在当前请求周期内立即读取刚写入数据库的记录。如果启用 sticky 选项,且当前请求周期内已对数据库执行了“写”操作,则后续的所有“读”操作都会使用“写”连接。这样可以确保在请求周期内写入的数据,能在同一请求期间被立即从数据库中读取。是否采用此行为,取决于你根据应用程序需求做出的决定。

防止过时的数据库读取

尽管 sticky 选项仅影响当前请求,但 Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads 中间件允许同一会话中的后续请求从写连接中读取最近写入的数据。这有助于避免用户在读副本追上写数据库之前看到过时数据。

若要启用此行为,请将中间件追加到应用程序 bootstrap/app.php 文件的 web 中间件组中:

use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        PreventStaleDatabaseReads::class,
    ]);
})

该中间件会在用户会话中跟踪对默认数据库连接的写入操作。默认情况下,在写入操作后的两秒内,对该连接的读取请求会使用写连接。你可以使用中间件的 for 方法自定义该时长:

$middleware->web(append: [
    PreventStaleDatabaseReads::for(5),
]);

由于该中间件依赖会话,因此必须在 Illuminate\Session\Middleware\StartSession 中间件之后执行。它不会影响其他会话发起的读取,也不会影响对其他数据库连接的读取。

读取时使用写连接

如果需要在执行回调时确保某个连接上的所有读取操作都使用写数据库,可以使用该连接的 usingWritePdo 方法:

use Illuminate\Support\Facades\DB;

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

这也适用于在使用相同连接的情况下,在回调内执行的 Eloquent 查询和关系查询。该方法会返回回调的结果,并在回调结束后恢复连接之前的读取偏好,即使抛出了异常也是如此。

池化的 PostgreSQL 连接

许多托管的 PostgreSQL 提供商通过 PgBouncer 或连接代理等服务提供事务模式连接池。这些连接池非常适合应用程序查询,但某些结构操作、迁移和维护命令需要直接访问数据库连接。

要在 PostgreSQL 中使用事务连接池,请照常配置池化连接,并通过 direct 配置选项提供直接连接的详细信息:

'pgsql' => [
    'driver' => 'pgsql',
    // ...
    'pooled' => env('DB_POOLED', false),
    'direct' => array_filter([
        'host' => env('DB_DIRECT_HOST'),
        'port' => env('DB_DIRECT_PORT'),
        'username' => env('DB_DIRECT_USERNAME'),
        'password' => env('DB_DIRECT_PASSWORD'),
        'sslmode' => env('DB_DIRECT_SSLMODE'),
    ]),
],

当 PostgreSQL 连接被配置为池化时,Laravel 会自动为该池化连接启用模拟预编译(emulated prepares)。直接连接会继承未在 direct 配置中显式定义的任意选项,并且默认使用原生预编译(native prepares)。

Laravel 会自动将直接连接用于迁移、结构导出和恢复、db:wipe、db:show 以及 db:table。启用池化模式并配置了直接连接时,db 命令默认也会使用直接连接;你可以传入 --pooled 选项来改为连接池化连接:

php artisan db --pooled

如果你需要在应用程序中显式使用直接连接,请在连接名称后追加 ::direct 后缀:

DB::connection('pgsql::direct')->statement('create extension if not exists "uuid-ossp"');

执行 SQL 查询

配置好数据库连接后,你可以使用 DB 门面来执行查询。DB 门面为每种类型的查询提供了相应的方法:select、update、insert、delete 和 statement。

执行 Select 查询

要执行基础的 SELECT 查询,你可以在 DB 门面上使用 select 方法:

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * 显示应用程序中所有用户的列表。
     */
    public function index(): View
    {
        $users = DB::select('select * from users where active = ?', [1]);

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

传递给 select 方法的第一个参数是 SQL 查询语句,第二个参数则是需要绑定到查询中的参数绑定。通常,这些是 where 子句约束条件的值。参数绑定提供了防止 SQL 注入的保护。

select 方法始终会返回一个结果 array。该数组内的每个结果都是一个代表数据库记录的 PHP stdClass 对象:

use Illuminate\Support\Facades\DB;

$users = DB::select('select * from users');

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

选择标量值

有时你的数据库查询可能只会得出一个单一的标量值。Laravel 允许你直接使用 scalar 方法来检索该值,而无需从记录对象中提取查询的标量结果:

$burgers = DB::scalar(
    "select count(case when food = 'burger' then 1 end) as burgers from menu"
);

选择多个结果集

如果你的应用程序调用了会返回多个结果集的存储过程,你可以使用 selectResultSets 方法来获取存储过程返回的所有结果集:

[$options, $notifications] = DB::selectResultSets(
    "CALL get_user_options_and_notifications(?)", [$request->user()->id]
);

使用命名绑定

你可以使用命名绑定来执行查询,而不是使用 ? 来表示参数绑定:

$results = DB::select('select * from users where id = :id', ['id' => 1]);

执行 Insert 语句

要执行 insert 语句,你可以在 DB 门面上使用 insert 方法。和 select 一样,该方法的第一个参数是 SQL 查询语句,第二个参数是绑定值:

use Illuminate\Support\Facades\DB;

DB::insert('insert into users (id, name) values (?, ?)', [1, 'Marc']);

执行 Update 语句

应该使用 update 方法来更新数据库中现有的记录。该方法会返回受该语句影响的行数:

use Illuminate\Support\Facades\DB;

$affected = DB::update(
    'update users set votes = 100 where name = ?',
    ['Anita']
);

执行 Delete 语句

应该使用 delete 方法来删除数据库中的记录。和 update 一样,该方法会返回受影响的行数:

use Illuminate\Support\Facades\DB;

$deleted = DB::delete('delete from users');

执行通用语句

某些数据库语句不会返回任何值。对于这类操作,你可以在 DB 门面上使用 statement 方法:

DB::statement('drop table users');

执行未预编译的语句

有时你可能希望在不绑定任何值的情况下执行 SQL 语句。你可以使用 DB 门面的 unprepared 方法来实现这一点:

DB::unprepared('update users set votes = 100 where name = "Dries"');

由于未预编译的语句不会绑定参数,它们可能会面临 SQL 注入的风险。你永远不应在未预编译的语句中允许受用户控制的值。

隐式提交

在事务中使用时,DB 门面的 statement 和 unprepared 方法,你必须小心避免使用会导致隐式提交的语句。这些语句会导致数据库引擎间接提交整个事务,使得 Laravel 无法感知数据库的事务级别。此类语句的一个例子是创建数据库表:

DB::unprepared('create table a (col varchar(1) null)');

请参阅 MySQL 手册,查看会触发隐式提交的全部语句列表。

使用多个数据库连接

如果你的应用程序在 config/database.php 配置文件中定义了多个连接,你可以使用 DB 门面提供的 connection 方法来访问每个连接。传递给 connection 方法的连接名称应当与 config/database.php 配置文件中列出的某个连接相对应,或是通过 config 辅助函数在运行时配置的连接:

use Illuminate\Support\Facades\DB;

$users = DB::connection('sqlite')->select(/* ... */);

你可以使用连接实例上的 getPdo 方法,来访问连接的原生底层 PDO 实例:

$pdo = DB::connection()->getPdo();

在 Vitess 上执行分析查询

当使用 Vitess 数据库(如 PlanetScale)时,你可以使用 olap 方法,利用 OLAP 工作负载来执行分析查询:

use Illuminate\Support\Facades\DB;

$totals = DB::connection('mysql')->olap(fn ($connection) => $connection->table('orders')
    ->selectRaw('customer_id, sum(total) as total')
    ->groupBy('customer_id')
    ->get()
);

Laravel 会同时在读写连接上设置工作负载,然后在回调完成后将其重置为 OLTP,即使抛出了异常也是如此。olap 方法会返回回调的结果。对于未使用 Vitess 的 MySQL 连接,回调执行时不会更改工作负载。

你可以通过 isVitess 方法来判断某个 MySQL 连接是否正在使用 Vitess:

$isVitess = DB::connection('mysql')->isVitess();

监听查询事件

如果你希望指定一个闭包,使其在应用程序执行每一条 SQL 查询时被调用,可以使用 DB 门面的 listen 方法。该方法对于记录查询或调试非常有用。你可以在服务提供者的 boot 方法中注册查询监听器闭包:

<?php

namespace App\Providers;

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用服务。
     */
    public function register(): void
    {
        // ...
    }

    /**
     * 引导任何应用服务。
     */
    public function boot(): void
    {
        DB::listen(function (QueryExecuted $query) {
            // $query->sql;
            // $query->bindings;
            // $query->time;
            // $query->toRawSql();
        });
    }
}

监控累计查询时间

现代 Web 应用的一个常见性能瓶颈是查询数据库所花费的时间。幸运的是,当 Laravel 在一次请求中查询数据库耗时过长时,可以调用你指定的闭包或回调函数。要开始使用,请向 whenQueryingForLongerThan 方法提供一个查询时间阈值(以毫秒为单位)和闭包。你可以在服务提供者的 boot 方法中调用此方法:

<?php

namespace App\Providers;

use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Events\QueryExecuted;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用服务。
     */
    public function register(): void
    {
        // ...
    }

    /**
     * 引导任何应用服务。
     */
    public function boot(): void
    {
        DB::whenQueryingForLongerThan(500, function (Connection $connection, QueryExecuted $event) {
            // 通知开发团队……
        });
    }
}

数据库事务

你可以使用 DB 门面提供的 transaction 方法,在数据库事务中运行一组操作。如果事务闭包内抛出异常,事务将自动回滚,并重新抛出该异常。如果闭包执行成功,事务将自动提交。使用 transaction 方法时,你无需担心手动回滚或提交:

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    DB::update('update users set votes = 1');

    DB::delete('delete from posts');
});

处理死锁

transaction 方法接受一个可选的第二个参数,用于指定在发生死锁时事务应重试的次数。一旦耗尽这些尝试次数,将抛出异常:

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    DB::update('update users set votes = 1');

    DB::delete('delete from posts');
}, attempts: 5);

手动使用事务

如果你想手动开启一个事务,并完全控制回滚和提交操作,可以使用 DB 门面提供的 beginTransaction 方法:

use Illuminate\Support\Facades\DB;

DB::beginTransaction();

你可以通过 rollBack 方法回滚事务:

DB::rollBack();

最后,你可以通过 commit 方法提交事务:

DB::commit();

DB 门面的事务方法可同时控制查询构造器和Eloquent ORM的事务。

连接数据库 CLI

如果你想连接到数据库的 CLI,可以使用 db Artisan 命令:

php artisan db

如有需要,你可以指定数据库连接名称,以连接到非默认连接的数据库:

php artisan db mysql

检查数据库

使用 db:show 和 db:table Artisan 命令,你可以获取有关数据库及其相关表的宝贵信息。若要查看数据库概览,包括其大小、类型、打开连接数以及表摘要,可以使用 db:show 命令:

php artisan db:show

你可以通过 --database 选项向命令提供数据库连接名称,指定要检查的数据库连接:

php artisan db:show --database=pgsql

如果你想在命令输出中包含表的行数和数据库视图详情,可以分别使用 --counts 和 --views 选项。对于大型数据库,获取行数和视图详情可能会比较慢:

php artisan db:show --counts --views

此外,你可以使用以下 Schema 方法来检查数据库:

use Illuminate\Support\Facades\Schema;

$tables = Schema::getTables();
$views = Schema::getViews();
$columns = Schema::getColumns('users');
$indexes = Schema::getIndexes('users');
$foreignKeys = Schema::getForeignKeys('users');

如果你想检查非应用默认连接的数据库连接,可以使用 connection 方法:

$columns = Schema::connection('sqlite')->getColumns('users');

表概览

如果你想获取数据库中单个表的概览,可以执行 db:table Artisan 命令。此命令提供数据库表的大致概览,包括其列、类型、属性、键和索引:

php artisan db:table users

监控数据库

使用 db:monitor Artisan 命令,当数据库正在管理的打开连接数超过指定数量时,你可以指示 Laravel 派发 Illuminate\Database\Events\DatabaseBusy 事件。

要开始使用,你应该调度 db:monitor 命令每分钟运行一次。该命令接受你要监控的数据库连接配置的名称,以及在派发事件之前允许的最大打开连接数:

php artisan db:monitor --databases=mysql,pgsql --max=100

仅仅调度此命令并不足以触发通知,以提醒你打开连接的数量。当命令遇到打开连接数超过阈值的数据库时,将派发 DatabaseBusy 事件。你应该在应用的 AppServiceProvider 中监听此事件,以便向你或你的开发团队发送通知:

use App\Notifications\DatabaseApproachingMaxConnections;
use Illuminate\Database\Events\DatabaseBusy;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;

/**
 * 引导任何应用服务。
 */
public function boot(): void
{
    Event::listen(function (DatabaseBusy $event) {
        Notification::route('mail', '[email protected]')
            ->notify(new DatabaseApproachingMaxConnections(
                $event->connectionName,
                $event->connections
            ));
    });
}