> For the complete documentation index, see [llms.txt](https://monksoul.gitbook.io/hoa/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://monksoul.gitbook.io/hoa/saasduozhuhu.md).

# 二十八、SaaS 多租户

## 什么是 SaaS

SaaS是Software-as-a-Service（软件即服务）的简称，随着互联网技术的发展和应用软件的成熟， 在21世纪开始兴起的一种完全创新的软件应用模式。它与“on-demand software”，the application service provider(ASP，应用服务提供商)，hosted software(托管软件)所具有相似的含义。

它是一种通过Internet提供软件的模式，厂商将应用软件统一部署在自己的服务器上，客户可以根据自己实际需求，通过互联网向厂商定购所需的应用软件服务，按定购的服务多少和时间长短向厂商支付费用，并通过互联网获得厂商提供的服务。用户不用再购买软件，而改用向提供商租用基于Web的软件，来管理企业经营活动，且无需对软件进行维护，服务提供商会全权管理和维护软件，软件厂商在向客户提供互联网应用的同时，也提供软件的离线操作和本地数据存储，让用户随时随地都可以使用其定购的软件和服务。

对于许多小型企业来说，SaaS是采用先进技术的最好途径，它消除了企业购买、构建和维护基础设施和应用程序的需要。

## 什么是多租户

多租户技术或称多重租赁技术，简称SaaS，是一种软件架构技术，是实现如何在多用户环境下（此处的多用户一般是面向企业用户）共用相同的系统或程序组件，并且可确保各用户间数据的隔离性。

简单讲：在一台服务器上运行单个应用实例，它为多个租户（客户）提供服务。从定义中我们可以理解：多租户是一种架构，目的是为了让多用户环境下使用同一套程序，且保证用户间数据隔离。那么重点就很浅显易懂了，多租户的重点就是同一套程序下实现多用户数据的隔离。

## 实现多租户方案

### 独立数据库

这是第一种方案，即一个租户一个数据库，这种方案的用户数据隔离级别最高，安全性最好，但成本较高。&#x20;

#### 优点：&#x20;

为不同的租户提供独立的数据库，有助于简化数据模型的扩展设计，满足不同租户的独特需求；如果出现故障，恢复数据比较简单。&#x20;

#### 缺点：&#x20;

增多了数据库的安装数量，随之带来维护成本和购置成本的增加。 这种方案与传统的一个客户、一套数据、一套部署类似，差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户，可以选择这种模式，提高租用的定价。如果定价较低，产品走低价路线，这种方案一般对运营商来说是无法承受的。

### 共享数据库，独立 Schema

&#x20;这是第二种方案，即多个或所有租户共享Database，但是每个租户一个Schema（也可叫做一个user）。底层库比如是：DB2、ORACLE等，一个数据库下可以有多个SCHEMA&#x20;

#### 优点：&#x20;

为安全性要求较高的租户提供了一定程度的逻辑数据隔离，并不是完全隔离；每个数据库可支持更多的租户数量。&#x20;

#### 缺点：&#x20;

如果出现故障，数据恢复比较困难，因为恢复数据库将牵涉到其他租户的数据； 如果需要跨租户统计数据，存在一定困难。

### 共享数据库，共享 Schema

共享数据表 这是第三种方案，即租户共享同一个Database、同一个Schema，但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。 即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据。&#x20;

#### 优点：&#x20;

三种方案比较，第三种方案的维护和购置成本最低，允许每个数据库支持的租户数量最多。&#x20;

#### 缺点：

&#x20;隔离级别最低，安全性最低，需要在设计开发时加大对安全的开发量； 数据备份和恢复最困难，需要逐表逐条备份和还原。

## 如何使用

在 Hoa Framework 框架中，默认采用了第三种方案实现多租户模式，即：**共享数据库，共享Schema。**&#x5177;体配置如下：

### 第一步

修改配置文件 `appsetting.json` 文件，启用 `EnableTenantMode: true`。

### 第二步

所有的模型都必须直接或间接继承 `IEntity<PrimaryKeyType>`，如：

```csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Hoa.Core.Entities
{
    [Table("Employee")]
    public class Employee : IEntity<int>
    {
        [MaxLength(32)]
        public string Name { get; set; }
        [MaxLength(10)]
        public string Gender { get; set; }
        public int Age { get; set; }
    }
}
```

### 第三步

在 `HoaDbContext` 中，配置 `DbSet<Tenant>` ，并新增 `GetTenantId()` 方法。

```csharp
using Hoa.DbManager.Tenant;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;

namespace Hoa.EntityFrameworkCore
{
    public partial class HoaDbContext : DbContext
    {
        public HoaDbContext(DbContextOptions<HoaDbContext> options)
            : base(options)
        {
        }

        // 配置多租户DbSet
        public virtual DbSet<Tenant> Tenants { get; set; }

        // 新增 GetTenantId 方法
        public Guid GetTenantId(string host)
        {
            var tenant = Tenants.FirstOrDefault(t => t.Host == host);
            return tenant?.Id ?? Guid.Empty;
        }

        // Other Codes
    }
}
```

### 第四步

在 `Hoa.Core.HoaCoreModule.cs` 中注册 `ITenantProvider` 实例类

```csharp
using Autofac;
using Hoa.DbManager.ContextPool;
using Hoa.DbManager.Repositories;
using Hoa.DbManager.Tangents;
using Hoa.DbManager.Tenant;

namespace Hoa.Core
{
    public class HoaCoreModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            // 注册多租户Id提供器
            builder.RegisterType<TenantProvider>()
            .As<ITenantProvider>()
            .InstancePerLifetimeScope();
        }
    }
}
```

### 第五步

在 `HoaDbContext` 中注入 `ITenantProvider`并设置全局过滤器，如：

```csharp
using Hoa.DbManager.Tenant;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;

namespace Hoa.EntityFrameworkCore
{
    public partial class HoaDbContext : DbContext
    {
        private Guid _tenantId;
        // 注入 ITenantProvider 
        public HoaDbContext(DbContextOptions<HoaDbContext> options, ITenantProvider tenantProvider)
            : base(options)
        {
            _tenantId = tenantProvider.GetTenantId();
        }

        public virtual DbSet<Tenant> Tenants { get; set; }

        public Guid GetTenantId(string host)
        {
            var tenant = Tenants.FirstOrDefault(t => t.Host == host);
            return tenant?.Id ?? Guid.Empty;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer("Name=HoaDatabase");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // 配置全局过滤器
            modelBuilder.Entity<Employee>().HasQueryFilter(b => EF.Property<Guid>(b, "TenantId") == _tenantId);

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}
```

## 使用说明

通过上面五步配置后，**新增、查询**操作的时候会自动应用租户Id并插入到表中。
