在 確定分佈策略 中,
我們討論了在多租戶用例中使用 Citus 所需的與框架無關的資料庫更改。
當前部分研究如何構建與 Citus 儲存後端一起使用的多租戶 ASP.NET 應用程式。
示例應用
為了使這個遷移部分具體化,
讓我們考慮一個簡化版本的 StackExchange。
供參考,最終結果存在於 Github 上。
Schema
我們將從兩張表開始:
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');
接下來包括一些測試資料。
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 或更高版本。
使用 dotnet new
從 MVC 模板建立一個新專案:
dotnet new mvc -o QuestionExchange
cd QuestionExchange
如果您願意,可以使用 dotnet run
預覽模板站點。
MVC 模板幾乎包含您開始使用的所有內容,但 Postgres 支援並不是開箱即用的。
你可以通過安裝 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));
您還需要在檔案頂部新增這些宣告:
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 來避免將資料庫憑據儲存在程式碼中(並意外將它們檢入原始碼控制中)。
接下來,您需要定義一個資料庫上下文。
新增 Tenancy(租賃) 到 App
定義 Entity Framework Core 上下文和模型
資料庫上下文類提供程式碼和資料庫之間的介面。
Entity Framework Core 使用它來了解您的 data
schema 是什麼樣的,
因此您需要定義資料庫中可用的表。
在專案根目錄中建立一個名為 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) 變得容易,
並且足夠靈活以處理許多不同的多租戶用例。
安裝 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);
}
此查詢獲取此租戶的最新五個問題(當然,現在只有一個問題)並填充檢視模型。
對於大型應用程式,您通常會將資料訪問程式碼放在 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...
為避免編譯器報錯,請在檔案頂部新增以下宣告:
using Microsoft.EntityFrameworkCore;
測試應用程式
您新增到資料庫的測試租戶與(fake)域 bufferoverflow.local
和 dboverflow.local
相關聯。
您需要 編輯 hosts 檔案 以在本地計算機上測試這些:
127.0.0.1 bufferoverflow.local
127.0.0.1 dboverflow.local
使用 dotnet run
或單擊 Visual Studio 中的 Start 啟動專案,
應用程式將開始偵聽 localhost:5000
之類的 URL。
如果您直接訪問該 URL,您將看到一個錯誤,因為您尚未設定任何 預設租戶行為。
相反,訪問 http://bufferoverflow.local:5000,
您將看到您的多租戶應用程式的一個租戶!
切換到 http://dboverflow.local:5000 檢視其他租戶。
新增更多租戶現在只需在 tenants
表中新增更多行即可。