Laravel 实现多租户的一些思考

前言

前段时间一直在研究 Laravel 多租户相关的开发,写篇文章记录下一些实现思路和关键代码。

需求起源

创业的项目需要给每个合作的学校提供教学系统等服务,而且需要做到每个学校的前端访问入口不一样。为了方便管理,最开始的计划是给每个学校的前端单独部署,分配独立的访问域名,所有前端均访问统一的后台。

这个时候,问题来了:每个学校的应用数据是相对独立的,怎么实现从数据层面分隔学校,以及通过前端请求映射到对应学校的数据,才能更「优雅」?

解决方案

如果你用谷歌搜索「Laravel multi tenacy」,可靠的方案并不多,其中有一个 tenancy/multi-tenant 乍一看还不错。仔细研究了下文档,发现教程「又臭又长」,而且对 Laravel 是有点侵入式开发的意味,所以只好放弃。

分库隔离

分库隔离是一开始我的首选,每个学校的数据库均独立。为了区分学校,需要在每个学校的前端系统请求后端 API 的时候,带上一个「学校参数」。

开发 Laravel 中间件,截取该参数识别出学校,然后配置对应的数据库。凡是带上有效「学校参数」的请求,所有增删改查都将局限在学校独立的数据库中。

做得更彻底一点,可以每个学校的前后端都单独部署,同时内部后台要可以连接每个学校的数据库进行数据汇总,以便做一些数据报表相关的工作。

这个方案其实是我最心水的,以我现在使用的架构来看,多个学校应用的单独部署/升级等管理都比较轻松。然而,由于现有系统的种种局限,比如后台拆分、权限拆分,以及其它若干功能不好界定归属于学校还是管理后台,这个方案还是被我暂时枪毙了。

字段隔离

既然前一个方案被 PASS 了,那也只剩下最简单粗暴的「字段隔离」方法了,在每一个学校有关的资源加上「school_id」区分。

考虑到,学校系统的资源以及功能要远多于管理端的,如果学校资源的增删改查都要加上「school_id」的处理,怕不是要疯,又谈何优雅?

当然,这也是有解决办法的。首先说一下整体方案:

  1. 每个学校的前端单独部署,配置不同的访问域名和「学校参数」,API 请求统一的后端并在请求头部带上「学校参数」
  2. 后端需要给 Laravel 开发一个中间件,能够提取「学校参数」,并将识别到的学校信息写入到 SESSION 中
  3. 利用 Laravel Eloquent Model 的特性,写一个 InSchool Trait,所有引入该 Trait 的 Model 在增删改查时,会自动处理「学校参数」,Trait 的实现方式可以参考 SoftDeletes

下面给一下 Trait 的参考实现,算是备忘,也给有需要的同胞一点思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Illuminate\Database\Eloquent\Builder;

trait InSchool
{
public static function bootInSchool()
{
if (session()->has('current_school')) {
static::addGlobalScope('school_id', function (Builder $builder) {
$builder->where('school_id', session()->get('current_school')->id);
});
}

static::saving(function ($model) {
if (!$model->school_id && session()->has('current_school')) {
$model->school_id = session()->get('current_school')->id;
}
});
}
}

小结

有一些无关紧要的感慨:做开发,应该尽量做一些「一劳永逸」的设计,而不是纯粹地堆砌业务代码。

做开发或者设计,不敢追求面面俱到(或者说完美无缺),否则会陷入一个细节无法自拔,进度不前。但也不能在能预见到风险和大可能性的改动时,还只顾着赶进度,否则不是把自己坑了就是把后人坑了。

最后,还有一点很重要:写代码最重要的是看得开!