在构建多租户或分库系统的时候,很多团队希望根据URL里的租户标识自动切换到对应的数据库。Laravel本身提供了多数据库连接支持,但默认连接是在配置文件里写死的。通过动态数据库路由参数,我们可以在请求进入时就决定使用哪一个connection,从而避免在每个模型里手动指定。

理解Laravel数据库连接解析机制
Laravel的数据库管理器通过DatabaseManager来维护多个连接实例。配置文件中定义的每一个连接,例如mysql_tenant_a,都会在被第一次使用时通过连接器创建PDO实例。当我们调用模型的setConnection方法,或者在查询构造器上使用connection方法时,实际上是在告诉管理器应该使用哪个已命名的连接。
默认情况下,config/database.php里的default决定了没有显式指定时的连接。但在路由层面,我们完全可以利用app('db')->extend或者运行时向配置仓库写入新连接,来达成动态效果。例如,在服务提供者中监听路由参数,然后将新的连接参数合并进配置,这样后续所有使用该名称的查询都会指向新库。
需要厘清的是,动态路由参数并不是修改default这么简单。如果直接改默认连接,并发请求会互相覆盖。正确做法是为每个租户生成独立的连接名,比如tenant_123,并在模型里通过getConnectionName返回该名称。这样不同请求之间完全隔离,不会污染全局状态。
通过中间件实现路由参数注入连接
最干净的实现方式是为租户相关路由注册一个中间件。在handle方法中,我们从$request->route('tenant')拿到参数,然后到配置中动态添加连接。下面示例展示如何在中间件里完成这件事。
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateSupportFacadesConfig;
class TenantDatabaseSwitcher
{
public function handle($request, Closure $next)
{
$tenant = $request->route('tenant');
if ($tenant) {
$connectionName = 'tenant_' . $tenant;
if (!Config::has('database.connections.' . $connectionName)) {
Config::set('database.connections.' . $connectionName, [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => 'db_' . $tenant,
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]);
}
// 将解析出的连接名存到请求属性,方便模型读取
$request->attributes->set('db_connection', $connectionName);
}
return $next($request);
}
}
上面代码在每次请求时检查是否存在对应租户的连接配置,没有就动态写入。这样做的好处是无需重启服务,也无需在配置文件罗列成百上千个库。但要注意,频繁写配置虽然发生在内存中,仍建议在连接创建成功后做缓存,避免重复解析。
模型侧只需要重写getConnectionName即可自动使用注入的连接。例如基类TenantModel中可以写:return request()->attributes->get('db_connection', 'mysql');。这样业务模型继承后完全不用关心租户逻辑,路由参数就是唯一的切换依据。
与静态配置方案的对比及避坑要点
静态配置方案是在config/database.php中把所有可能的租户库都列出来,再通过RouteServiceProvider用Route::middleware绑定。这种方式在租户少时直观,但一旦超过几十个,配置文件膨胀且无法应对动态新建租户。运行时动态注入则灵活得多,配合数据库里的租户表还能做到按需建连。
| 方案 | 扩展性 | 维护成本 | 隔离性 |
|---|---|---|---|
| 静态配置 | 低 | 高 | 好 |
| 动态路由参数 | 高 | 低 | 好(独立连接名) |
常见误区之一是在控制器里调用DB::setDefaultConnection。由于Laravel的默认连接是进程级单例,在PHP-FPM或Swoole等长生命周期环境下,这次请求改了默认库,下一次请求可能继续用错误的库。务必使用独立连接名而不是动全局默认。
另一个坑是忘记在队列任务中传递连接信息。如果请求中动态切了库,但派发的job在另一个进程执行,它读不到原请求的属性。解决办法是把connectionName作为job构造参数存入,在job里显式setConnection。只有把动态参数当成一等公民在上下文间传递,整套路由参数驱动的分库才稳。
Laravel动态数据库路由database_connection修改时间:2026-08-17 16:40:26