🎨 完美优雅的处理多租户-分表方案

1.扩展原有的MultiTenantAttribute 标识多库、多表
2.扩展原有的种子数据生成 用于多表的种子数据
3.巧妙优雅使用Sqlsugar表映射 解决多租户分表问题,原有代码无需改动 登录用户如果是租户用户自动切换到租户分表

目前来看(如果想要升级业务 扩展SAAS)
多表方案:代码侵入最小
id方案:侵入最大,需要增加列
多库方案:相对少

如果是从0到1 最推荐多库
如果是从0.5到1 最推荐多表
This commit is contained in:
Lemon.NoCry 2023-02-21 01:50:51 +08:00
parent 9b2506101a
commit bd484137a6
20 changed files with 503 additions and 70 deletions

View File

@ -98,7 +98,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\Tenant\" />
<Folder Include="wwwroot\BlogCore.Data.excel\" />
</ItemGroup>

View File

@ -311,27 +311,6 @@
博客文章 评论
</summary>
</member>
<member name="T:Blog.Core.Model.Models.BusinessTable">
<summary>
业务数据 <br/>
多租户 (Id 隔离)
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.TenantId">
<summary>
无需手动赋值
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.Name">
<summary>
名称
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.Amount">
<summary>
金额
</summary>
</member>
<member name="T:Blog.Core.Model.Models.Department">
<summary>
部门表
@ -917,22 +896,6 @@
软删除 过滤器
</summary>
</member>
<member name="T:Blog.Core.Model.Models.SubLibraryBusinessTable">
<summary>
多租户-多库方案 业务表 <br/>
公共库无需标记[MultiTenant]特性
</summary>
</member>
<member name="P:Blog.Core.Model.Models.SubLibraryBusinessTable.Name">
<summary>
名称
</summary>
</member>
<member name="P:Blog.Core.Model.Models.SubLibraryBusinessTable.Amount">
<summary>
金额
</summary>
</member>
<member name="T:Blog.Core.Model.Models.SysTenant">
<summary>
系统租户表 <br/>
@ -1145,6 +1108,63 @@
任务内存中的状态
</summary>
</member>
<member name="T:Blog.Core.Model.Models.BusinessTable">
<summary>
业务数据 <br/>
多租户 (Id 隔离)
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.TenantId">
<summary>
无需手动赋值
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.Name">
<summary>
名称
</summary>
</member>
<member name="P:Blog.Core.Model.Models.BusinessTable.Amount">
<summary>
金额
</summary>
</member>
<member name="T:Blog.Core.Model.Models.MultiBusinessSubTable">
<summary>
多租户-多表方案 业务表 子表 <br/>
</summary>
</member>
<member name="T:Blog.Core.Model.Models.MultiBusinessTable">
<summary>
多租户-多表方案 业务表 <br/>
</summary>
</member>
<member name="P:Blog.Core.Model.Models.MultiBusinessTable.Name">
<summary>
名称
</summary>
</member>
<member name="P:Blog.Core.Model.Models.MultiBusinessTable.Amount">
<summary>
金额
</summary>
</member>
<member name="T:Blog.Core.Model.Models.SubLibraryBusinessTable">
<summary>
多租户-多库方案 业务表 <br/>
公共库无需标记[MultiTenant]特性
</summary>
</member>
<member name="P:Blog.Core.Model.Models.SubLibraryBusinessTable.Name">
<summary>
名称
</summary>
</member>
<member name="P:Blog.Core.Model.Models.SubLibraryBusinessTable.Amount">
<summary>
金额
</summary>
</member>
<member name="T:Blog.Core.Model.Models.Topic">
<summary>
Tibug 类别
@ -1894,8 +1914,9 @@
</member>
<member name="T:Blog.Core.Model.Tenants.MultiTenantAttribute">
<summary>
标识 多租户-分库 的业务表 <br/>
公共表无需区分 直接使用主库 各自业务在各自库中
标识 多租户 的业务表 <br/>
默认设置是多库 <br/>
公共表无需区分 直接使用主库 各自业务在各自库中 <br/>
</summary>
</member>
<member name="T:Blog.Core.Model.Tenants.TenantTypeEnum">
@ -1913,6 +1934,11 @@
库隔离
</summary>
</member>
<member name="F:Blog.Core.Model.Tenants.TenantTypeEnum.Tables">
<summary>
表隔离
</summary>
</member>
<member name="T:Blog.Core.Model.ViewModels.AdvertisementViewModels">
<summary>
广告类

View File

@ -1238,7 +1238,7 @@
</member>
<member name="T:Blog.Core.Api.Controllers.Tenant.TenantByDbController">
<summary>
多租户测试
多租户-多库方案 测试
</summary>
</member>
<member name="M:Blog.Core.Api.Controllers.Tenant.TenantByDbController.GetAll">
@ -1270,6 +1270,23 @@
</summary>
<returns></returns>
</member>
<member name="T:Blog.Core.Api.Controllers.Tenant.TenantByTableController">
<summary>
多租户-多表方案 测试
</summary>
</member>
<member name="M:Blog.Core.Api.Controllers.Tenant.TenantByTableController.GetAll">
<summary>
获取租户下全部业务数据 <br/>
</summary>
<returns></returns>
</member>
<member name="M:Blog.Core.Api.Controllers.Tenant.TenantByTableController.Post(Blog.Core.Model.Models.MultiBusinessTable)">
<summary>
新增数据
</summary>
<returns></returns>
</member>
<member name="T:Blog.Core.Api.Controllers.Tenant.TenantManagerController">
<summary>
租户管理

View File

@ -0,0 +1,57 @@
using Blog.Core.Common.HttpContextUser;
using Blog.Core.Controllers;
using Blog.Core.IServices.BASE;
using Blog.Core.Model;
using Blog.Core.Model.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Blog.Core.Api.Controllers.Tenant;
/// <summary>
/// 多租户-多表方案 测试
/// </summary>
[Produces("application/json")]
[Route("api/Tenant/ByTable")]
[Authorize]
public class TenantByTableController : BaseApiController
{
private readonly IBaseServices<MultiBusinessTable> _services;
private readonly IUser _user;
public TenantByTableController(IUser user, IBaseServices<MultiBusinessTable> services)
{
_user = user;
_services = services;
}
/// <summary>
/// 获取租户下全部业务数据 <br/>
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<MessageModel<List<MultiBusinessTable>>> GetAll()
{
//查询
// var data = await _services.Query();
//关联查询
var data = await _services.Db
.Queryable<MultiBusinessTable>()
.Includes(s => s.Child)
.ToListAsync();
return Success(data);
}
/// <summary>
/// 新增数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<MessageModel> Post(MultiBusinessTable data)
{
await _services.Db.Insertable(data).ExecuteReturnSnowflakeIdAsync();
return Success();
}
}

View File

@ -39,6 +39,10 @@ public class RepositorySetting
return;
}
//多租户 单表
db.QueryFilter.AddTableFilter<ITenantEntity>(it => it.TenantId == App.User.TenantId || it.TenantId == 0);
//多租户 多表
db.SetTenantTable(App.User.TenantId.ToString());
}
}

View File

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Blog.Core.Model.Models;
using Blog.Core.Model.Tenants;
using SqlSugar;
namespace Blog.Core.Common.DB;
@ -19,7 +23,7 @@ public static class TenantUtil
switch (tenant.DbType.Value)
{
case DbType.Sqlite:
tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db" ;
tenant.Connection = $"DataSource={Path.Combine(Environment.CurrentDirectory, tenant.ConfigId)}.db";
break;
}
}
@ -47,4 +51,52 @@ public static class TenantUtil
},
};
}
public static List<Type> GetTenantEntityTypes(TenantTypeEnum? tenantType = null)
{
return RepositorySetting.Entitys
.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass)
.Where(s => IsTenantEntity(s, tenantType))
.ToList();
}
public static bool IsTenantEntity(this Type u, TenantTypeEnum? tenantType = null)
{
var mta = u.GetCustomAttribute<MultiTenantAttribute>();
if (mta is null)
{
return false;
}
if (tenantType != null)
{
if (mta.TenantType != tenantType)
{
return false;
}
}
return true;
}
public static string GetTenantTableName(this Type type, ISqlSugarClient db, string id)
{
var entityInfo = db.EntityMaintenance.GetEntityInfo(type);
return $@"{entityInfo.DbTableName}_{id}";
}
public static string GetTenantTableName(this Type type, ISqlSugarClient db, SysTenant tenant)
{
return GetTenantTableName(type, db, tenant.Id.ToString());
}
public static void SetTenantTable(this ISqlSugarClient db, string id)
{
var types = GetTenantEntityTypes(TenantTypeEnum.Tables);
foreach (var type in types)
{
db.MappingTables.Add(type.Name, type.GetTenantTableName(db, id));
}
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Blog.Core.Common.Extensions;
public static class UntilExtensions
{
public static void AddOrModify<TKey, TValue>(this IDictionary<TKey, TValue> dic, TKey key, TValue value)
{
if (dic.TryGetValue(key, out _))
{
dic[key] = value;
}
else
{
dic.Add(key, value);
}
}
}

View File

@ -2,8 +2,10 @@
using Blog.Core.Common.Extensions;
using Blog.Core.Common.Helper;
using Blog.Core.Model.Models;
using Blog.Core.Model.Tenants;
using Magicodes.ExporterAndImporter.Excel;
using Newtonsoft.Json;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.IO;
@ -11,8 +13,6 @@ using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Blog.Core.Model.Tenants;
using SqlSugar;
namespace Blog.Core.Common.Seed
{
@ -422,19 +422,62 @@ namespace Blog.Core.Common.Seed
public static async Task TenantSeedAsync(MyContext myContext)
{
var tenants = await myContext.Db.Queryable<SysTenant>().Where(s => s.TenantType == TenantTypeEnum.Db).ToListAsync();
if (!tenants.Any())
if (tenants.Any())
{
return;
Console.WriteLine($@"Init Multi Tenant Db");
foreach (var tenant in tenants)
{
Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}");
await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig());
}
}
Console.WriteLine($@"Init Multi Tenant Db");
foreach (var tenant in tenants)
tenants = await myContext.Db.Queryable<SysTenant>().Where(s => s.TenantType == TenantTypeEnum.Tables).ToListAsync();
if (tenants.Any())
{
Console.WriteLine($@"Init Multi Tenant Db : {tenant.ConfigId}/{tenant.Name}");
await InitTenantSeedAsync(myContext.Db.AsTenant(), tenant.GetConnectionConfig());
await InitTenantSeedAsync(myContext, tenants);
}
}
#region
private static async Task InitTenantSeedAsync(MyContext myContext, List<SysTenant> tenants)
{
ConsoleHelper.WriteInfoLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId}");
// 获取所有实体表-初始化租户业务表
var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Tables);
if (!entityTypes.Any()) return;
foreach (var sysTenant in tenants)
{
foreach (var entityType in entityTypes)
{
myContext.Db.CodeFirst
.As(entityType, entityType.GetTenantTableName(myContext.Db, sysTenant))
.InitTables(entityType);
Console.WriteLine($@"Init Tables:{entityType.GetTenantTableName(myContext.Db, sysTenant)}");
}
myContext.Db.SetTenantTable(sysTenant.Id.ToString());
//多租户初始化种子数据
await TenantSeedDataAsync(myContext.Db, TenantTypeEnum.Tables);
}
ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Tables : {myContext.Db.CurrentConnectionConfig.ConfigId} created successfully!");
}
#endregion
#region
/// <summary>
/// 初始化多库
/// </summary>
/// <param name="itenant"></param>
/// <param name="config"></param>
/// <returns></returns>
public static async Task InitTenantSeedAsync(ITenant itenant, ConnectionConfig config)
{
itenant.RemoveConnection(config.ConfigId);
@ -445,12 +488,10 @@ namespace Blog.Core.Common.Seed
db.DbMaintenance.CreateDatabase();
ConsoleHelper.WriteSuccessLine($"Init Multi Tenant Db : {config.ConfigId} Database created successfully!");
Console.WriteLine($@"Init Multi Tenant Db : {config.ConfigId} Create Tables");
// 获取所有实体表-初始化租户业务表
var entityTypes = RepositorySetting.Entitys
.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass)
.Where(s => s.IsDefined(typeof(MultiTenantAttribute), false));
var entityTypes = TenantUtil.GetTenantEntityTypes(TenantTypeEnum.Db);
if (!entityTypes.Any()) return;
foreach (var entityType in entityTypes)
{
@ -464,10 +505,12 @@ namespace Blog.Core.Common.Seed
}
//多租户初始化种子数据
await TenantSeedDataAsync(db);
await TenantSeedDataAsync(db, TenantTypeEnum.Db);
}
private static async Task TenantSeedDataAsync(ISqlSugarClient db)
#endregion
private static async Task TenantSeedDataAsync(ISqlSugarClient db, TenantTypeEnum tenantType)
{
// 获取所有种子配置-初始化数据
var seedDataTypes = AssemblysExtensions.GetAllAssemblies().SelectMany(s => s.DefinedTypes)
@ -481,12 +524,7 @@ namespace Blog.Core.Common.Seed
}
var eType = esd.GenericTypeArguments[0];
if (eType.GetCustomAttribute<MultiTenantAttribute>() is null)
{
return false;
}
return true;
return eType.IsTenantEntity(tenantType);
});
if (!seedDataTypes.Any()) return;
foreach (var seedType in seedDataTypes)

View File

@ -0,0 +1,38 @@
using Blog.Core.Model.Models;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Blog.Core.Common.Seed.SeedData;
public class MultiBusinessDataSeedData : IEntitySeedData<MultiBusinessTable>
{
public IEnumerable<MultiBusinessTable> InitSeedData()
{
return new List<MultiBusinessTable>()
{
new()
{
Id = 1001,
Name = "业务数据1",
Amount = 100,
},
new()
{
Id = 1002,
Name = "业务数据2",
Amount = 1000,
},
};
}
public IEnumerable<MultiBusinessTable> SeedData()
{
return default;
}
public Task CustomizeSeedData(ISqlSugarClient db)
{
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,38 @@
using Blog.Core.Model.Models;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Blog.Core.Common.Seed.SeedData;
public class MultiBusinessSubDataSeedData : IEntitySeedData<MultiBusinessSubTable>
{
public IEnumerable<MultiBusinessSubTable> InitSeedData()
{
return new List<MultiBusinessSubTable>()
{
new()
{
Id = 100,
MainId = 1001,
Memo = "子数据",
},
new()
{
Id = 1001,
MainId = 1001,
Memo = "子数据2",
},
};
}
public IEnumerable<MultiBusinessSubTable> SeedData()
{
return default;
}
public Task CustomizeSeedData(ISqlSugarClient db)
{
return Task.CompletedTask;
}
}

View File

@ -50,6 +50,13 @@ public class TenantSeedData : IEntitySeedData<SysTenant>
DbType = DbType.Sqlite,
Connection = $"DataSource=" + Path.Combine(Environment.CurrentDirectory, "ZhaoLiu.db"),
},
new SysTenant()
{
Id = 1000005,
ConfigId = "Tenant_5",
Name = "孙七",
TenantType = TenantTypeEnum.Tables,
},
};
}

View File

@ -54,6 +54,14 @@ public class UserInfoSeedData : IEntitySeedData<SysUserInfo>
Name = "赵六",
TenantId = 1000004, //租户Id
},
new SysUserInfo()
{
Id = 10005,
LoginName = "sunqi",
LoginPWD = "E10ADC3949BA59ABBE56E057F20F883E",
Name = "孙七",
TenantId = 1000005, //租户Id
},
};
var names = data.Select(s => s.LoginName).ToList();

View File

@ -0,0 +1,14 @@
using Blog.Core.Model.Models.RootTkey;
using Blog.Core.Model.Tenants;
namespace Blog.Core.Model.Models;
/// <summary>
/// 多租户-多表方案 业务表 子表 <br/>
/// </summary>
[MultiTenant(TenantTypeEnum.Tables)]
public class MultiBusinessSubTable : BaseEntity
{
public long MainId { get; set; }
public string Memo { get; set; }
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Blog.Core.Model.Models.RootTkey;
using Blog.Core.Model.Tenants;
using SqlSugar;
namespace Blog.Core.Model.Models;
/// <summary>
/// 多租户-多表方案 业务表 <br/>
/// </summary>
[MultiTenant(TenantTypeEnum.Tables)]
public class MultiBusinessTable : BaseEntity
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 金额
/// </summary>
public decimal Amount { get; set; }
[Navigate(NavigateType.OneToMany, nameof(MultiBusinessSubTable.MainId))]
public List<MultiBusinessSubTable> Child { get; set; }
}

View File

@ -3,10 +3,22 @@
namespace Blog.Core.Model.Tenants;
/// <summary>
/// 标识 多租户-分库 的业务表 <br/>
/// 公共表无需区分 直接使用主库 各自业务在各自库中
/// 标识 多租户 的业务表 <br/>
/// 默认设置是多库 <br/>
/// 公共表无需区分 直接使用主库 各自业务在各自库中 <br/>
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class MultiTenantAttribute : Attribute
{
public MultiTenantAttribute()
{
}
public MultiTenantAttribute(TenantTypeEnum tenantType)
{
TenantType = tenantType;
}
public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Db;
}

View File

@ -20,4 +20,10 @@ public enum TenantTypeEnum
/// </summary>
[Description("库隔离")]
Db = 2,
/// <summary>
/// 表隔离
/// </summary>
[Description("表隔离")]
Tables = 3,
}

View File

@ -1,6 +1,10 @@
using Blog.Core.Common;
using Blog.Core.Common.DB;
using Blog.Core.IRepository.Base;
using Blog.Core.Model;
using Blog.Core.Model.Models;
using Blog.Core.Model.Tenants;
using Blog.Core.Repository.UnitOfWorks;
using SqlSugar;
using System;
using System.Collections.Generic;
@ -8,11 +12,6 @@ using System.Data;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Blog.Core.Common.DB;
using Blog.Core.Common.HttpContextUser;
using Blog.Core.Model.Models;
using Blog.Core.Model.Tenants;
using Blog.Core.Repository.UnitOfWorks;
namespace Blog.Core.Repository.Base
{
@ -45,7 +44,8 @@ namespace Blog.Core.Repository.Base
}
//多租户
if (typeof(TEntity).GetCustomAttribute<MultiTenantAttribute>() != null)
var mta = typeof(TEntity).GetCustomAttribute<MultiTenantAttribute>();
if (mta is { TenantType: TenantTypeEnum.Db })
{
//获取租户信息 租户信息可以提前缓存下来
if (App.User is { TenantId: > 0 })

View File

@ -0,0 +1,73 @@
using System;
using Autofac;
using Blog.Core.Common.Extensions;
using Blog.Core.IRepository.Base;
using Blog.Core.Model.Models;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Math;
using SqlSugar;
using Xunit;
using Xunit.Abstractions;
namespace Blog.Core.Tests;
public class OrmTest
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly IBaseRepository<BlogArticle> _baseRepository;
DI_Test dI_Test = new DI_Test();
public OrmTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
var container = dI_Test.DICollections();
_baseRepository = container.Resolve<IBaseRepository<BlogArticle>>();
_baseRepository.Db.Aop.OnLogExecuting = (sql, p) =>
{
_testOutputHelper.WriteLine("");
_testOutputHelper.WriteLine("==================FullSql=====================", "", new string[] { sql.GetType().ToString(), GetParas(p), "【SQL语句】" + sql });
_testOutputHelper.WriteLine("【SQL语句】" + sql);
_testOutputHelper.WriteLine(GetParas(p));
_testOutputHelper.WriteLine("==============================================");
_testOutputHelper.WriteLine("");
};
}
private static string GetParas(SugarParameter[] pars)
{
string key = "【SQL参数】";
foreach (var param in pars)
{
key += $"{param.ParameterName}:{param.Value}\n";
}
return key;
}
[Fact]
public void MultiTables()
{
var sql = _baseRepository.Db.Queryable<BlogArticle>()
.AS($@"{nameof(BlogArticle)}_TenantA")
.ToSqlString();
//_testOutputHelper.WriteLine(sql);
_baseRepository.Db.MappingTables.Add(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA");
var query = _baseRepository.Db.Queryable<BlogArticle>()
.LeftJoin<BlogArticleComment>((a, c) => a.bID == c.bID);
// query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticle), $@"{nameof(BlogArticle)}_TenantA");
//query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA");
// query.QueryBuilder.AsTables.AddOrModify(nameof(BlogArticleComment), $@"{nameof(BlogArticleComment)}_TenantA");
// query.QueryBuilder.AsTables.AddOrModify(nameof(SysUserInfo), $@"{nameof(SysUserInfo)}_TenantA");
sql = query.ToSqlString();
_testOutputHelper.WriteLine(sql);
sql = _baseRepository.Db.Deleteable<BlogArticle>().ToSqlString();
_testOutputHelper.WriteLine(sql);
}
}