# 二十八、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并插入到表中。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://monksoul.gitbook.io/hoa/saasduozhuhu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
