ShardingCore是一款efcore下高性能、轻量级针对分表分库读写分离的框架。正好多租户需要用到分库分表,因此选择了这个框架,其官方文档提供了详细的多租户教程,我实现的过程中绝大部分照搬了这个教程。最后架子搭起来,写个文章记录一下,也让自己理解的深入一些。
1. 多租户
多租户主要有三种实现的方式,多数据库,单数据库多架构(Schema),单数据库单架构。简单来说就是不同的租户的数据是存在不同的数据库,或者一个数据库的不同架构,或者仅仅以表中的某个字段来区分,由此可以很容易的理清三种方案在成本、编写逻辑、维护、数据备份与恢复等方面的优劣势。
因为ShardingCore并没有直接提供分架构的用法,我又想采取折中方案,即不同的租户的数据存储在相同数据库的不同的Schema中,所以我的设想是使用ShardingCore分库的方法来实现分架构,然后我失败了。这里需要简单介绍一下我的数据库,项目选用的是PostgreSQL,因为要存储地理数据,因此数据库加装了postgis
扩展,项目中引用了Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite
库。这时候问题来了,PostgreSQL数据库中postgis扩展只能属于一个架构,没法所有架构都用,而我每个租户有各自的地理数据,因此没法采用这种方案,最终选择了成本最高,隔离性和安全性最好的多数据库的方案。
2. 租户与用户
这部分是我与官方教程比较大的区别。我的想法是租户是租户,用户是用户,一个租户可以有很多个用户,租户的管理员只是租户用户中的一个,因此,租户存在主数据库中,用户存在租户数据库中。
主数据库的DbContext
public class DefaultDbContext: DbContext
{
public DefaultDbContext(DbContextOptions<DefaultDbContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
base.OnConfiguring(builder);
}
/// <summary>
/// 租户表
/// </summary>
public DbSet<Tenant> Tenants {
get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
string assemblyName = AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).FullName;
Assembly assembly = Assembly.Load(assemblyName);
foreach (var type in assembly.GetTypes())
{
var baseclass = type.BaseType;
if (baseclass == typeof(EntityBase))
{
//种子数据
MethodInfo method = type.GetMethod("HasData");
if (method != null)
{
var data = (IEnumerable<object>)method.Invoke(null, null);
builder.Entity(type.FullName).HasData(data);
}
else
{
builder.Entity(type.FullName);
}
}
}
}
}
租户数据库的DbContext
public class TenantDbContext: AbstractShardingDbContext
{
public TenantDbContext(DbContextOptions<TenantDbContext> options) : base(options)
{
}
/// <summary>
/// 用户表
/// </summary>
public DbSet<User> Users {
get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
string assemblyName = AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).FullName;
Assembly assembly = Assembly.Load(assemblyName);
foreach (var type in assembly.GetTypes())
{
var baseclass = type.BaseType;
if (baseclass == typeof(TenantEntityBase))
{
//种子数据
MethodInfo method = type.GetMethod("HasData");
if (method != null)
{
var data = (IEnumerable<object>)method.Invoke(null, null);
modelBuilder.Entity(type.FullName).HasData(data);
}
else
{
modelBuilder.Entity(type.FullName);
}
}
}
}
这里TenantDbContext
继承自AbstractShardingDbContext,是ShardingCore分库的配置。
我的想法是系统的超级管理员也只是系统中一个特别的租户而已,也就是这个租户可以管理其他租户,类似于二房东,也住这栋楼里。所以超级管理员登录,跟一个普通租户的普通用户登录应该没有什么区别,因此我额外加了一个常量,来记录这个二房东的ID。
/// <summary>
/// 租户相关的常量
/// </summary>
public static class TenantConstant
{
/// <summary>
/// 系统租户ID
/// </summary>
public const long SYSTEM_TENANT_ID = 1;
}
3. 租户与DbContext
每个租户是不同的数据库,有不同的连接字符串,因此不能如下直接注入,得对每个租户分别注入TenantDbContext。
builder.Services.AddDbContext<DefaultDbContext>(options => {
options.UseNpgsql(builder.Configuration.GetConnectionString("NpgContext"));
});
因为使用了ShardingCore,他允许通过IShardingRuntimeContext
来进行注入。首先构造一个租户与注入参数的中间类:
/// <summary>
/// 租户分库配置
/// </summary>
public class ShardingTenantOptions
{
/// <summary>
/// 默认数据源名称
/// </summary>
public string DefaultDataSourceName {
get; set; }
/// <summary>
/// 默认数据库地址
/// </summary>
public string DefaultConnectionString {
get; set; }
/// <summary>
/// 分片迁移的命名空间关键字
/// </summary>
public string MigrationNamespace {
get; set; }
}
创建一个IShardingRuntimeContext
构建器
public class ShardingBuilder : IShardingBuilder
{
public static readonly ILoggerFactory efLogger = LoggerFactory.Create(builder