在我们<<Laravel底层实战兼核心源码解析>>这个课程的第九章:<Laravel 国际前沿实践探究>

file file

我们"邀请"了来自美国的大咖Tom给我们系统讲解了如何用Laravel来构建一个SAAS多租户平台 file file

这期间Tom系统讲解了什么是SAAS多租户平台,为什么你要使用这种架构,在构建SAAS平台时单数据库方案和多数据库方案之间有何优劣对比,该如何基于情况去选择,当然也探讨了SAAS平台下如何去处理队列\命令行\搜索,以及外部服务等,最后还介绍了几个构建SAAS时的优秀插件.

可以说Tom的专场,已经涵盖了用laravel搞SAAS平台的方方面面,相信看过的小伙伴对SAAS架构已经胸有成竹了.那么这篇文章呢,我们只是关注SAAS平台搭建中的一个方面,当然也是最令人困惑的一个方面,就是多个数据库之间的通信与切换,我们重点关注一下这一点,通过这一点来深入了解laravel背后处理数据库连接的方式和原理.至于SAAS架构其他的方面,如果你想搞,那么还是免不了要仔细研究Tom的专场.

大部分的应用,只有一个数据库,只需要跟单个数据库进行交互.但是啊,也有相当一部分laravel应用,需要处理多个数据库之间的交互.虽然这方面有一些不错的组件,但是深入理解一下laravel里数据库连接的原理,还是非常有帮助的.

创建数据库连接

当你在laravel里执行一个数据查询,是Illuminate\Database\DatabaseManager在具体负责设置好相应的数据库连接.在配置里,不同的数据库连接有不同的名字,你可以选一个作为默认的数据库连接.这样当你没有提供具体连接数据库的名字时,就可以用默认的那个.

// 这样用的是默认的连接
DB::table('users')->all();

// 这里声明了使用"tenant" 这个数据库(连接)
DB::connection('tenant')->table('users')->all();

这个数据库连接在一个laravel生命或请求周期里,只会创建一次,也即是一个单例模式,这样整个期间,只用这一个数据库连接就可以了,既保证效率,又避免混乱.

PDO----PHP标准数据对象

PDO是PHP里跟数据库进行交互时的一个标准接口,laravel也是使用了PDO来进行各种的数据查询.当然了,你也可以再配置个数据库连接,然后用它来进行独立的PDO读写逻辑,这样就相当于一个数据库是用来查询或读取的,而另一个数据库是专门用来执行写入\删除\更新等的逻辑.当然更多的,可以进一步查看laravel读写分离的官方文档

大部分的"多租户"应用,都会给每个"租户"或机构单独设置一个数据库,然后再有一个总的\处于中央位置的数据库,这个数据库用来存储一些租户的整体细节信息.那么这样的话,在一个单一的应用里,你就会同时有一个"系统级"的数据库连接,然后还会有一个"租户"或机构级别的数据库连接.

'tenant' => [
  'driver' => 'mysql',
  'host' => env('DB_HOST', '127.0.0.1'),
  'port' => env('DB_PORT', '3306'),
  // ...
],

'system' => [
  'driver' => 'mysql',
  'host' => env('DB_HOST', '127.0.0.1'),
  'port' => env('DB_PORT', '3306'),
  // ...
],

这个系统级别的\总的数据库连接,总是连到那同一个数据库,所以它在config文件里的具体配置是不变的,这个连接下的查询也很简单,都可以类似这样来进行:

DB::connection('system')->table('tenants')->all();

但是当你要在一个租户的数据库上进行查询和连接的时候,就会有意思起来了.因为要具体连接到哪一个租户的数据库,取决于系统当前的租户是谁.因为没法提前知道这一点,所以我们也就不可能在config/database.php文件里具体设置好或者说"穷尽"租户的数据库连接.所以呢,租户或机构的数据库连接,就必须在运行中进行动态设置了.

config(['database.connections.tenant.database' => 'tenant1']);

上面的这行代码,就会将tenant这个数据库连接的配置,具体指向到"tenant1" 这个数据库,用同样的方式,你也可以更改其他的数据库连接配置参数,比如username, password, read/write connections等等.

那么现在,当DatabaseManager想着创建tenant相应的连接时,就会用你刚才动态设置的配置项.但是呢,假设在这之前,这个tenant的数据库连接已经解析过一次了,也即里面具体的配置已经被laravel缓存(cache)了,那么这个时候新更改的设置就不会生效,也就不会创建一个新的数据库连接.

要解决这个问题啊,你得确保在设置新的数据库配置项之前,系统里没有其它的\已经解析过或生效了的数据库连接:

config(['database.connections.tenant.database' => 'tenant1']);

DB::purge('tenant');

DB::reconnect('tenant');

使用purge()和reconnect()方法,可以确保在tenant这个连接通道上,接下来的任何新的数据查询,都会用上面这个最新设置的数据库信息.

当然了,这个地方我们只是关注数据库的重新连接,如果你看过我们的<<Laravel底层实战兼核心源码解析>>课程,看过Tom关于Laravel SAAS的专场,那么这个地方其实还可以搞一些其他必要的事情,比如设置app.name,设置app.url,同时触发一些有用的event什么的.虽然其他的细节不是我们这篇文章的关注点,但是也要提醒你不要限制想象力哦

这些代码具体要写在哪里呢?

一个laravel程序,实际上有好几个"入口":

  1. http请求(HTTP Requests)
  2. 命令行(Console Commands)
  3. 队列任务(Queued Jobs)

我们可以创建一个TenancyServiceProvider,记得将其添加到config/app.php里,那么在其register方法里,我们就可以这样来动态设置当前tenant的数据库连接信息了:

public function register(){
    if($this->app->runningInConsole()){
        return;
    }

    if($request->getHttpHost() == 'tenant1.app.com'){
      config(['database.connections.tenant.database' => 'tenant1']);
    
      DB::purge('tenant');
    
      DB::reconnect('tenant');
     }
}

检查当前http请求的host信息,然后基于此,来将数据库连接,设置成相应租户的.

至于队列job,我们可以把tenant_id信息存到所有job的相应payload里.这样当具体执行这个job时,就可以用之前的方式来动态修改数据库连接配置了.所以在service provider里,可以再添加这么一行:

$this->app['queue']->createPayloadUsing(function () {
      return Tenant::get() ? [
              'tenant_id' => Tenant::get()->id
             ] : [];
});

这里的Tenant::get()是自己写的,用来判断或获取当前的tenant是哪个.这样的话,每一个job的payload里都会包含一个tenant_id的信息,这时我们就可以监听JobProcessing这个事件,然后来相应地配置数据库连接:

$this->app['events']->listen(\Illuminate\Queue\Events\JobProcessing::class, function($event){
    if (isset($event->job->payload()['tenant_id'])) {
        Tenant::set($event->job->payload()['tenant_id']);
    }
});

至于命令行里,你得声明当前的tenant是谁,比如通过参数的形式.