Laravel 13 数据库入门:连接、SQL 查询、事务与监控

简介

几乎所有现代 Web 应用都会与数据库交互。Laravel 通过原生 SQL、流式查询构造器和 Eloquent ORM,简化多种数据库的访问。当前章节列出的第一方支持范围如下:

MongoDB 通过由 MongoDB 官方维护的 mongodb/laravel-mongodb 包获得支持,详见 Laravel MongoDB 文档。这些是原章节列出的最低版本,不等于相应数据库版本仍在安全维护期。

配置

数据库服务的配置位于应用的 config/database.php。这里可以定义所有数据库连接,并指定默认连接。大多数选项取自应用的环境变量;该文件包含 Laravel 所支持的大多数数据库系统的配置示例。

默认的环境配置示例可直接用于 Laravel Sail。Sail 提供本地开发 Laravel 应用的 Docker 配置。也可以按自己的本地数据库环境修改配置。

SQLite 配置

SQLite 数据库保存在文件系统中的单个文件内。可以在终端执行 touch database/database.sqlite 创建数据库文件。创建后,把绝对路径写入 DB_DATABASE 环境变量:

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

SQLite 连接默认启用外键约束。若需要禁用,设置:

DB_FOREIGN_KEYS=false

如果使用 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,例如:

mysql://root:password@127.0.0.1/forge?charset=UTF-8

此类 URL 通常遵循以下结构:

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

Laravel 支持用这种 URL 替代多个独立的配置选项。如果设置了 url 选项,或对应的 DB_URL 环境变量,Laravel 会从中提取数据库连接与凭据信息。上面的 password 只是原文中的示例占位值。

读写连接

有时希望让 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 的数组;其他数据库选项从主 mysql 配置合并而来。

只有希望覆盖主 mysql 数组中的值时,才需要把相应配置放入 read 或 write。原文以 192.168.1.1 为读取主机、192.168.1.3 为写入主机说明这一点。数据库凭据、前缀、字符集及其他主配置由两个连接共享。host 数组含多个值时,每次请求会随机选择一个数据库主机。

示例里的 196.168.1.2 按官方源代码原样保留;它与其余 192.168.* 地址不同,配置自己的环境时应逐项换成真实的数据库主机,不能把这些地址当成可直接连接的服务。

sticky 选项

sticky 是可选项,用于在当前请求周期内立即读取刚写入的数据。启用它后,只要当前请求执行过写操作,后续读操作就会使用写连接。这样,同一次请求写入的数据能在该请求中立即读回。是否需要这种行为,应由应用的业务要求决定;它不保证下一次请求或其他连接的复制一致性。

PostgreSQL 连接池

很多托管 PostgreSQL 服务通过 PgBouncer 等服务或连接代理提供事务模式的连接池。这适合应用查询,但部分 schema 操作、迁移和维护命令需要直接连接数据库。

使用事务连接池时,正常配置池化连接,并通过 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 会自动为池化连接启用模拟预处理。直连连接继承 direct 中未明确覆盖的选项,默认使用原生预处理。

Laravel 会自动为迁移、schema 导出与恢复,以及 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 facade 执行查询。它为不同操作提供 select、update、insert、delete、statement 等方法。

执行 SELECT 查询

执行基本 SELECT 查询时,使用 DB facade 的 select 方法:

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * Show a list of all of the application's users.
     */
    public function index(): View
    {
        $users = DB::select('select * from users where active = ?', [1]);

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

第一个参数是 SQL,第二个参数是要绑定到查询的值,通常是 where 条件中的约束值。参数绑定有助于防止 SQL 注入。

select 始终返回结果数组,每个元素是代表一条记录的 PHP stdClass 对象:

use Illuminate\Support\Facades\DB;

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

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

查询标量值

如果查询只返回一个标量值,可以用 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 的第一个参数是 SQL,第二个参数是绑定值,与 select 相同:

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 删除记录。它同样返回影响的行数:

use Illuminate\Support\Facades\DB;

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

该示例没有 where,会删除表中全部用户;只在隔离的演示数据库中使用。

执行一般语句

有些语句不返回查询结果,此时使用 statement:

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

该示例会删除整张表,属于教学示例,不应直接用于业务库。

执行未预处理语句

如果需要执行不绑定任何值的 SQL,可以使用 unprepared:

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

未预处理语句不绑定参数,可能引入 SQL 注入。绝不能把用户可控值放入这样的语句中。

隐式提交

在事务内使用 statement、unprepared 时,要避开会触发隐式提交的语句。这些语句会让数据库引擎间接提交整个事务,而 Laravel 无法获知数据库事务层级已发生变化。例如:

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

触发隐式提交的完整语句清单,参见 MySQL 官方手册。

使用多个连接

若 config/database.php 定义了多个连接,可以通过 DB::connection 访问。传入的连接名应对应配置文件中的连接,或通过 config helper 在运行时配置的连接:

use Illuminate\Support\Facades\DB;

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

调用连接实例的 getPdo,可以访问底层原始 PDO 实例:

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

监听查询事件

要为应用执行的每条 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
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        DB::listen(function (QueryExecuted $query) {
            // $query->sql;
            // $query->bindings;
            // $query->time;
            // $query->toRawSql();
        });
    }
}

监控累计查询时间

一次请求花在查询数据库上的总时间,是常见的性能瓶颈。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
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        DB::whenQueryingForLongerThan(500, function (Connection $connection, QueryExecuted $event) {
            // Notify development team...
        });
    }
}

这里的 500 是毫秒阈值,衡量单次请求的累计查询耗时,并不是单条 SQL 的独立超时。

数据库事务

DB::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);

闭包可能重新执行,写库之外的发邮件、支付调用等外部副作用,应另行处理幂等性。

手动控制事务

如果要完全控制提交和回滚,可以手动开启事务:

use Illuminate\Support\Facades\DB;

DB::beginTransaction();

回滚:

DB::rollBack();

提交:

DB::commit();

DB facade 的事务方法同时控制查询构造器和 Eloquent ORM 使用的事务。

连接数据库命令行

通过 Artisan 的 db 命令进入数据库 CLI:

php artisan db

如需连接非默认数据库,传入连接名:

php artisan db mysql

检查数据库

db:show、db:table 可用于了解数据库和表。查看数据库大小、类型、当前连接数及表摘要:

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 提供单张表的列、类型、属性、键与索引概览:

php artisan db:table users

监控数据库

db:monitor 可以在开放连接数量超过设定值时,派发 Illuminate\Database\Events\DatabaseBusy 事件。

先将命令配置为每分钟运行一次。它接受要监控的连接名,以及触发事件前可容忍的最大开放连接数:

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;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Event::listen(function (DatabaseBusy $event) {
        Notification::route('mail', 'dev@example.com')
            ->notify(new DatabaseApproachingMaxConnections(
                $event->connectionName,
                $event->connections
            ));
    });
}

示例中的 DatabaseApproachingMaxConnections 是应用需要定义的通知类。记录 SQL 和绑定值时应脱敏,按业务需要控制日志范围。

来源与许可

原章节:Database: Getting Started,对应 Laravel 13.x 文档源。Copyright (c) Taylor Otwell。文档仓库采用 MIT License,许可全文随本地稿置于 assets/license.txt。

本稿将章节正文汉化,保留官方代码、字符串及英文注释原样,并补充了示例地址、危险 SQL、事务副作用和日志范围的说明。代码仅做静态比对,未在本机运行。

许可全文(英文原文)
The MIT License (MIT)

Copyright (c) Taylor Otwell

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容