在 确定分布策略 中, 我们讨论了在多租户用例中使用 Citus 所需的与框架无关的数据库更改。当前部分研究如何构建与 Citus 存储后端一起使用的多租户 ASP.NET 应用程序。
- http://citus.hacker-linner.com/develop/migration_mt_schema.html#mt-schema-migration
示例应用
为了使这个迁移部分具体化, 让我们考虑一个简化版本的 StackExchange。供参考,最终结果存在于 Github 上。
- https://github.com/nbarbettini/QuestionExchange
Schema
我们将从两张表开始:
代码语言:javascript复制
CREATE TABLE tenants (
id uuid NOT NULL,
domain text NOT NULL,
name text NOT NULL,
description text NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL
);
CREATE TABLE questions (
id uuid NOT NULL,
tenant_id uuid NOT NULL,
title text NOT NULL,
votes int NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL
);
ALTER TABLE tenants ADD PRIMARY KEY (id);
ALTER TABLE questions ADD PRIMARY KEY (id, tenant_id);
我们 demo 应用程序的每个租户都将通过不同的域名进行连接。ASP.NET Core 将检查传入请求并在 tenants
表中查找域。您还可以按子域(或您想要的任何其他 scheme)查找租户。
注意 tenant_id
是如何存储在 questions
表中的。这将使 :ref:colocate <colocation>
数据成为可能。创建表后,使用 create_distributed table
告诉 Citus 对租户 ID 进行分片:
SELECT create_distributed_table('tenants', 'id');
SELECT create_distributed_table('questions', 'tenant_id');
接下来包括一些测试数据。
代码语言:javascript复制
INSERT INTO tenants VALUES (
'c620f7ec-6b49-41e0-9913-08cfe81199af',
'bufferoverflow.local',
'Buffer Overflow',
'Ask anything code-related!',
now(),
now());
INSERT INTO tenants VALUES (
'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',
'dboverflow.local',
'Database Questions',
'Figure out why your connection string is broken.',
now(),
now());
INSERT INTO questions VALUES (
'347b7041-b421-4dc9-9e10-c64b8847fedf',
'c620f7ec-6b49-41e0-9913-08cfe81199af',
'How do you build apps in ASP.NET Core?',
1,
now(),
now());
INSERT INTO questions VALUES (
'a47ffcd2-635a-496e-8c65-c1cab53702a7',
'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',
'Using postgresql for multitenant data?',
2,
now(),
now());
这样就完成了数据库结构和示例数据。我们现在可以继续设置 ASP.NET Core。
ASP.NET Core 项目
如果您没有安装 ASP.NET Core,请安装 Microsoft 的 .NET Core SDK。这些说明将使用 dotnet
CLI, 但如果您使用的是 Windows, 也可以使用 Visual Studio 2017 或更高版本。
- https://dot.net/core
使用 dotnet new
从 MVC 模板创建一个新项目:
dotnet new mvc -o QuestionExchange
cd QuestionExchange
如果您愿意,可以使用 dotnet run
预览模板站点。MVC 模板几乎包含您开始使用的所有内容,但 Postgres 支持并不是开箱即用的。你可以通过安装 Npgsql.EntityFrameworkCore.PostgreSQL 包来解决这个问题:
- https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
此包将 Postgres 支持添加到 Entity Framework Core、ASP.NET Core 中的默认 ORM 和数据库层。打开 Startup.cs
文件并将这些行添加到 ConfigureServices
方法的任意位置:
var connectionString = "connection-string";
services.AddEntityFrameworkNpgsql()
.AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString));
您还需要在文件顶部添加这些声明:
代码语言:javascript复制using Microsoft.EntityFrameworkCore;
using QuestionExchange.Models;
将 connection-string
替换为您的 Citus 连接字符串。我的看起来像这样:
Server=myformation.db.citusdata.com;Port=5432;Database=citus;Userid=citus;Password=mypassword;SslMode=Require;Trust Server Certificate=true;
您可以使用 Secret Manager 来避免将数据库凭据存储在代码中(并意外将它们检入源代码控制中)。
- https://docs.microsoft.com/aspnet/core/security/app-secrets?tabs=visual-studio-code
接下来,您需要定义一个数据库上下文。
添加 Tenancy(租赁) 到 App
定义 Entity Framework Core 上下文和模型
数据库上下文类提供代码和数据库之间的接口。Entity Framework Core 使用它来了解您的 data schema 是什么样的, 因此您需要定义数据库中可用的表。
- https://msdn.microsoft.com/library/jj679962(v=vs.113).aspx#Anchor_2
在项目根目录中创建一个名为 AppDbContext.cs
的文件,并添加以下代码:
using System.Linq;
using Microsoft.EntityFrameworkCore;
using QuestionExchange.Models;
namespace QuestionExchange
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Tenant> Tenants { get; set; }
public DbSet<Question> Questions { get; set; }
}
}
两个 DbSet
属性指定用于对每个表的行建模的 C# 类。接下来您将创建这些类。在此之前,请在 Questions
属性下方添加一个新方法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var mapper = new Npgsql.NpgsqlSnakeCaseNameTranslator();
var types = modelBuilder.Model.GetEntityTypes().ToList();
// Refer to tables in snake_case internally
types.ForEach(e => e.Relational().TableName = mapper.TranslateMemberName(e.Relational().TableName));
// Refer to columns in snake_case internally
types.SelectMany(e => e.GetProperties())
.ToList()
.ForEach(p => p.Relational().ColumnName = mapper.TranslateMemberName(p.Relational().ColumnName));
}
C# 类和属性按惯例是 PascalCase,但 Postgres 表和列是小写的(和 snake_case)。 OnModelCreating
方法允许您覆盖默认名称转换并让 Entity Framework Core 知道如何在数据库中查找实体。
现在您可以添加代表租户和问题的类。在 Models 目录中创建一个 Tenant.cs
文件:
using System;
namespace QuestionExchange.Models
{
public class Tenant
{
public Guid Id { get; set; }
public string Domain { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
}
还有一个 Question.cs
文件,也在 Models 目录中:
using System;
namespace QuestionExchange.Models
{
public class Question
{
public Guid Id { get; set; }
public Tenant Tenant { get; set; }
public string Title { get; set; }
public int Votes { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
}
}
注意 Tenant
属性。在数据库中,问题表包含一个 tenant_id
列。Entity Framework Core 足够聪明,可以确定此属性表示租户和问题之间的一对多关系。稍后在查询数据时会用到它。
到目前为止,您已经设置了 Entity Framework Core 和与 Citus 的连接。下一步是向 ASP.NET Core 管道添加多租户支持。
安装 SaasKit
SaasKit 是一款优秀的开源 ASP.NET Core 中间件。该软件包使您的 Startup
请求管道 租户感知(tenant-aware) 变得容易, 并且足够灵活以处理许多不同的多租户用例。
- https://github.com/saaskit/saaskit
- http://benfoster.io/blog/asp-net-5-multitenancy
安装 SaasKit.Multitenancy 包:
- https://www.nuget.org/packages/SaasKit.Multitenancy/
dotnet add package SaasKit.Multitenancy
SaasKit 需要两件事才能工作:租户模型(tenant model)和租户解析器(tenant resolver)。您已经有了前者(您之前创建的 Tenant
类),因此在项目根目录中创建一个名为 CachingTenantResolver.cs
的新文件:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using SaasKit.Multitenancy;
using QuestionExchange.Models;
namespace QuestionExchange
{
public class CachingTenantResolver : MemoryCacheTenantResolver<Tenant>
{
private readonly AppDbContext _context;
public CachingTenantResolver(
AppDbContext context, IMemoryCache cache, ILoggerFactory loggerFactory)
: base(cache, loggerFactory)
{
_context = context;
}
// Resolver runs on cache misses
protected override async Task<TenantContext<Tenant>> ResolveAsync(HttpContext context)
{
var subdomain = context.Request.Host.Host.ToLower();
var tenant = await _context.Tenants
.FirstOrDefaultAsync(t => t.Domain == subdomain);
if (tenant == null) return null;
return new TenantContext<Tenant>(tenant);
}
protected override MemoryCacheEntryOptions CreateCacheEntryOptions()
=> new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromHours(2));
protected override string GetContextIdentifier(HttpContext context)
=> context.Request.Host.Host.ToLower();
protected override IEnumerable<string> GetTenantIdentifiers(TenantContext<Tenant> context)
=> new string[] { context.Tenant.Domain };
}
}
ResolveAsync
方法完成了繁重的工作:给定传入请求,它会查询数据库并查找与当前域匹配的租户。如果找到,它会将 TenantContext
传回给 SaasKit。所有租户解析逻辑完全取决于您 - 您可以按子域、路径或任何其他您想要的方式分隔租户。
此实现使用 租户缓存策略(tenant caching strategy) <http://benfoster.io/blog/aspnet-core-multi-tenancy-tenant-lifetime>
__, 因此您不会在每个传入请求上都使用租户查找来访问数据库。第一次查找后,租户将被缓存两个小时(您可以将其更改为任何有意义的内容)。
准备好租户模型(tenant model)和租户解析器(tenant resolver)后, 打开 Startup
类并在 ConfigureServices
方法中的任何位置添加此行:
services.AddMultitenancy<Tenant, CachingTenantResolver>();
接下来,将此行添加到 Configure
方法中,在 UseStaticFiles
下方但在 UseMvc
上方:
app.UseMultitenancy<Tenant>();
Configure
方法代表您的实际请求管道,因此顺序很重要!
更新视图
现在所有部分都已就绪,您可以开始在代码和视图中引用当前租户。打开 Views/Home/Index.cshtml
视图并用这个标记替换整个文件:
@inject Tenant Tenant
@model QuestionListViewModel
@{
ViewData["Title"] = "Home Page";
}
<div class="row">
<div class="col-md-12">
<h1>Welcome to <strong>@Tenant.Name</strong></h1>
<h3>@Tenant.Description</h3>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4>Popular questions</h4>
<ul>
@foreach (var question in Model.Questions)
{
<li>@question.Title</li>
}
</ul>
</div>
</div>
@inject
指令从 SaasKit 获取当前租户,并且 @model
指令告诉 ASP.NET Core, 此视图将由新模型类(您将创建)支持。在 Models 目录中创建 QuestionListViewModel.cs
文件:
using System.Collections.Generic;
namespace QuestionExchange.Models
{
public class QuestionListViewModel
{
public IEnumerable<Question> Questions { get; set; }
}
}
查询数据库
HomeController
负责渲染您刚刚编辑的索引视图。打开它并用这个替换 Index()
方法:
public async Task<IActionResult> Index()
{
var topQuestions = await _context
.Questions
.Where(q => q.Tenant.Id == _currentTenant.Id)
.OrderByDescending(q => q.UpdatedAt)
.Take(5)
.ToArrayAsync();
var viewModel = new QuestionListViewModel
{
Questions = topQuestions
};
return View(viewModel);
}
此查询获取此租户的最新五个问题(当然,现在只有一个问题)并填充视图模型。
代码语言:javascript复制对于大型应用程序,您通常会将数据访问代码放在 service 或 repository 层中,
并将其置于 controller 之外。这只是一个简单的例子!
您添加的代码需要 _context
和 _currentTenant
,这在 controller 中尚不可用。您可以通过以下方式提供这些向类添加构造函数:
public class HomeController : Controller
{
private readonly AppDbContext _context;
private readonly Tenant _currentTenant;
public HomeController(AppDbContext context, Tenant tenant)
{
_context = context;
_currentTenant = tenant;
}
// Existing code...
为避免编译器报错,请在文件顶部添加以下声明:
代码语言:javascript复制using Microsoft.EntityFrameworkCore;
测试应用程序
您添加到数据库的测试租户与(fake)域 bufferoverflow.local
和 dboverflow.local
相关联。您需要 编辑 hosts 文件 以在本地计算机上测试这些:
- https://www.howtogeek.com/howto/27350/beginner-geek-how-to-edit-your-hosts-file/
127.0.0.1 bufferoverflow.local
127.0.0.1 dboverflow.local
使用 dotnet run
或单击 Visual Studio 中的 Start 启动项目, 应用程序将开始侦听 localhost:5000
之类的 URL。如果您直接访问该 URL,您将看到一个错误,因为您尚未设置任何 默认租户行为。
- http://benfoster.io/blog/handling-unresolved-tenants-in-saaskit
相反,访问 http://bufferoverflow.local:5000, 您将看到您的多租户应用程序的一个租户!切换到 http://dboverflow.local:5000 查看其他租户。添加更多租户现在只需在 tenants
表中添加更多行即可。
更多
探索 Python/Django 支持分布式多租户数据库,如 Postgres Citus