CodeFirst同时映射多种数据库及分表优化设计
开发环境:Windows 10专业版、Visual Studio Code、.Net6、SqlSugar5.x、Navicat
根据需求,对数据处理时,部分用到Sqlite进行增删改查、部分用到MySql进行增删改查
问题1:对于程序集中的模型对象映射需要根据对应连接进行初始化处理
问题2:对于需要延迟进行模型初始化的特殊处理(根据字段自动分表数据模型)
1、通过ConfigId来区分数据库连接
扩展IoC容器,自定义SqlSugar的注入,一方面是代码复用,另外就是简化配置与代码可读性
builder.Services.SetupSqlSugar(options => { options.connectionConfigs = new List<ConnectionConfig> { new ConnectionConfig { ConnectionString = builder.Configuration.GetConnectionString("Sqlite"), DbType = DbType.Sqlite, ConfigId = "sqlite", InitKeyType = InitKeyType.Attribute, IsAutoCloseConnection = true }, new ConnectionConfig { ConnectionString = builder.Configuration.GetConnectionString("MySql"), DbType = DbType.MySql, ConfigId = "mysql", ConfigureExternalServices = new ConfigureExternalServices() { SplitTableService = new DefaultSplitTableService() }, InitKeyType = InitKeyType.Attribute, IsAutoCloseConnection = true } }; });
SetupSqlSugar函数原型:public static IServiceCollection SetupSqlSugar(this IServiceCollection services, Action<SqlSugarOptions> setup)
SqlSugarOptions是一个包装了ConnectionConfig的对象:
public class SqlSugarOptions { public List<ConnectionConfig> connectionConfigs { get; set; } }
对于IoC容器的扩展方法主要是使用asp.netcore中常用的配置模式,将配置对象填充需要的值后放置备用的思路,具体使用
services.AddOptions(); services.Configure(setup);
2、实现简单仓储模式
现实简单仓储模式的目的就是应对增删改查有足够灵活、全面的方法实现,这点上面SqlSugar简直是太贴心已经有比较好的实现(基本在实际应用中没有遇到短板)接口ISimpleClient
与实现SimpleClient
namespace SqlSugar { public interface ISimpleClient<T> where T : class, new() { SimpleClient<ChangeType> Change<ChangeType>() where ChangeType : class, new(); RepositoryType ChangeRepository<RepositoryType>() where RepositoryType : ISugarRepository; IDeleteable<T> AsDeleteable(); IInsertable<T> AsInsertable(List<T> insertObjs); IInsertable<T> AsInsertable(T insertObj); IInsertable<T> AsInsertable(T[] insertObjs); ISugarQueryable<T> AsQueryable(); ISqlSugarClient AsSugarClient(); ITenant AsTenant(); IUpdateable<T> AsUpdateable(List<T> updateObjs); IUpdateable<T> AsUpdateable(T updateObj); IUpdateable<T> AsUpdateable(); IUpdateable<T> AsUpdateable(T[] updateObjs); int Count(Expression<Func<T, bool>> whereExpression); bool Delete(Expression<Func<T, bool>> whereExpression); bool Delete(T deleteObj); bool Delete(List<T> deleteObjs); bool DeleteById(dynamic id); bool DeleteByIds(dynamic[] ids); T GetById(dynamic id); List<T> GetList(); List<T> GetList(Expression<Func<T, bool>> whereExpression); List<T> GetPageList(Expression<Func<T, bool>> whereExpression, PageModel page); List<T> GetPageList(Expression<Func<T, bool>> whereExpression, PageModel page, Expression<Func<T, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc); List<T> GetPageList(List<IConditionalModel> conditionalList, PageModel page); List<T> GetPageList(List<IConditionalModel> conditionalList, PageModel page, Expression<Func<T, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc); T GetSingle(Expression<Func<T, bool>> whereExpression); T GetFirst(Expression<Func<T, bool>> whereExpression); bool Insert(T insertObj); bool InsertOrUpdate(T data); bool InsertOrUpdate(List<T> datas); bool InsertRange(List<T> insertObjs); bool InsertRange(T[] insertObjs); int InsertReturnIdentity(T insertObj); long InsertReturnBigIdentity(T insertObj); long InsertReturnSnowflakeId(T insertObj); List<long> InsertReturnSnowflakeId(List<T> insertObjs); T InsertReturnEntity(T insertObj); bool IsAny(Expression<Func<T, bool>> whereExpression); bool Update(Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression); bool UpdateSetColumnsTrue(Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression); bool Update(T updateObj); bool UpdateRange(List<T> updateObjs); bool UpdateRange(T[] updateObjs); Task<int> CountAsync(Expression<Func<T, bool>> whereExpression); Task<bool> DeleteAsync(Expression<Func<T, bool>> whereExpression); Task<bool> DeleteAsync(T deleteObj); Task<bool> DeleteAsync(List<T> deleteObjs); Task<bool> DeleteByIdAsync(dynamic id); Task<bool> DeleteByIdsAsync(dynamic[] ids); Task<T> GetByIdAsync(dynamic id); Task<List<T>> GetListAsync(); Task<List<T>> GetListAsync(Expression<Func<T, bool>> whereExpression); Task<List<T>> GetPageListAsync(Expression<Func<T, bool>> whereExpression, PageModel page); Task<List<T>> GetPageListAsync(Expression<Func<T, bool>> whereExpression, PageModel page, Expression<Func<T, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc); Task<List<T>> GetPageListAsync(List<IConditionalModel> conditionalList, PageModel page); Task<List<T>> GetPageListAsync(List<IConditionalModel> conditionalList, PageModel page, Expression<Func<T, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc); Task<T> GetSingleAsync(Expression<Func<T, bool>> whereExpression); Task<T> GetFirstAsync(Expression<Func<T, bool>> whereExpression); Task<bool> InsertAsync(T insertObj); Task<bool> InsertOrUpdateAsync(T data); Task<bool> InsertOrUpdateAsync(List<T> datas); Task<bool> InsertRangeAsync(List<T> insertObjs); Task<bool> InsertRangeAsync(T[] insertObjs); Task<int> InsertReturnIdentityAsync(T insertObj); Task<long> InsertReturnBigIdentityAsync(T insertObj); Task<long> InsertReturnSnowflakeIdAsync(T insertObj); Task<List<long>> InsertReturnSnowflakeIdAsync(List<T> insertObjs); Task<T> InsertReturnEntityAsync(T insertObj); Task<bool> IsAnyAsync(Expression<Func<T, bool>> whereExpression); Task<bool> UpdateSetColumnsTrueAsync(Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression); Task<bool> UpdateAsync(Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression); Task<bool> UpdateAsync(T updateObj); Task<bool> UpdateRangeAsync(List<T> updateObjs); Task<bool> UpdateRangeAsync(T[] updateObjs); } }
基于这个方便设计一个仓储基类
public interface IRepositoryBase<T> : ISimpleClient<T> where T : class, new() { } public class RepositoryBase<T> : SimpleClient<T>, IRepositoryBase<T> where T : class, new() { public RepositoryBase(ISqlSugarClient context) : base(context) { } }
3、实现SetupSqlSugar扩展方法
SqlSugar中有提供一个TenantAttribute
的Attribute,考虑到SqlSugar有可能使用此Attribute进行其他的处理,稳妥起见实现一个CodeFirstAttribute
,通过反射枚举此Attribute来进行对应数据库的InitTables来进行数据库与对象表的自动建立
public static class SqlSugarSetup { public static IServiceCollection SetupSqlSugar(this IServiceCollection services, Action<SqlSugarOptions> setup) { services.AddOptions(); services.Configure(setup); return services.AddSingleton<ISqlSugarClient, SqlSugarScope>(provider => { var logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger("SqlSugar"); var options = provider.GetRequiredService<IOptions<SqlSugarOptions>>().Value; // 将ConnectionConfig列表中未指定ConfigId值设置为"default", 与CodeFirstAttribute进行关联 foreach (var config in options.connectionConfigs.Where(config => config.ConfigId == null)) config.ConfigId = "default"; var sugarScope = new SqlSugarScope(options.connectionConfigs); foreach (var item in options.connectionConfigs) { logger.LogInformation($"DbType: {item.DbType}, ConnectionString: {item.ConnectionString}"); } #if DEBUG sugarScope.Aop.OnLogExecuted = (sql, args) => { logger.LogInformation(sql, args); }; #endif sugarScope.Aop.OnError = ex => { logger.LogError(ex.InnerException?.Message); }; sugarScope.DbMaintenance.CreateDatabase(); List<Assembly> assemblies = AppDomain.CurrentDomain.GetCurrentAppAssemblies(); var types = assemblies.SelectMany(x => x.GetExportedTypes()) .Where(x => !x.IsAbstract && x.IsClass && x.IsDefined(typeof(CodeFirstAttribute))); foreach (var config in options.connectionConfigs) { var sugar = sugarScope.AsTenant().GetConnectionScope(config.ConfigId); var configuredTypes = types.Where(x => x.GetCustomAttribute<CodeFirstAttribute>()?.ConfigId.Equals(config.ConfigId, StringComparison.OrdinalIgnoreCase)) .ToArray(); sugar.CodeFirst.InitTables(configuredTypes); } return sugarScope; }); } }
其中CodeFirstAttribute
实现
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class CodeFirstAttribute: Attribute { /// <summary> /// 配置ID /// 用于区分不同的连接 /// </summary> public string ConfigId { get; set; } public CodeFirstAttribute(string configId = "default") { ConfigId = configId; } }
主要的作用是在反射扫描程序集时枚举CodeFirstAttribute
属性,并放入集合为下一步的逻辑处理提供支持
List<Assembly> assemblies = AppDomain.CurrentDomain.GetCurrentAppAssemblies(); var types = assemblies.SelectMany(x => x.GetExportedTypes()) .Where(x => !x.IsAbstract && x.IsClass && x.IsDefined(typeof(CodeFirstAttribute)));
依据数据库的ConfigId
来对实体映射到相应的数据库
foreach (var config in options.connectionConfigs) { var sugar = sugarScope.AsTenant().GetConnectionScope(config.ConfigId); var configuredTypes = types.Where(x => x.GetCustomAttribute<CodeFirstAttribute>()?.ConfigId.Equals(config.ConfigId, StringComparison.OrdinalIgnoreCase)) .ToArray(); sugar.CodeFirst.InitTables(configuredTypes); }
4、将租户模式集成到简单仓储模式
所使用的配置集合时,sqlite的配置是首个元素、mysql处于下一个元素,通过调试可以了解到注入到仓储接口中的ISqlSugarClient为集合中首个元素的实列
在实际的使用过程中不可能保证到哪个Connection放在首位,鉴于这类情况,为了保证简单仓库兼容租户模式的使用,需要将ISqlSugarClient定位到对应的对象context.AsTenant().GetConnectionScope(ConfigId)
至此,不同类型数据库租户加简单仓储模式已经完成,各个类型的实体和相应的数据库进行映射,自动维护表结构
5、根据表字段自动分表、自动联表操作
在MySql数据库中存在一个数据较大的表,为了优化操作考虑用分表来优化设计,对后期的操作提供性能上的优势;SqlSugar支持以时间分表(内置)、自定义分表的功能,我们这里需要进行分表的字段是非时间性的字段,所以选用自定义分表的功能;
-
需要在配置数据库时指定
ConnectionConfig.ConfigureExternalServices.SplitTableService
ConfigureExternalServices = new ConfigureExternalServices() { SplitTableService = new DefaultSplitTableService() } -
在需要自动分表的实体模型上面加上
[SplitTable(SplitType._Custom01)]
属性 -
在分表字段上面加上
[SplitField]
属性
另外,需要实现一个继承ISplitTableService
接口的服务,依次实现接口方法
public class DefaultSplitTableService : ISplitTableService { public List<SplitTableInfo> GetAllTables(ISqlSugarClient db, EntityInfo EntityInfo, List<DbTableInfo> tableInfos) { var splitType = EntityInfo.Type.GetCustomAttribute<SplitTableAttribute>()?.SplitType; var tableName = EntityInfo.DbTableName; List<SplitTableInfo> result = new List<SplitTableInfo>(); if (splitType == null || splitType != SplitType._Custom01) return result; foreach (var tableInfo in tableInfos) { if (tableInfo.Name.Contains(tableName, StringComparison.OrdinalIgnoreCase)) { SplitTableInfo info = new SplitTableInfo(); info.TableName = tableInfo.Name; result.Add(info); } } return result; } public object GetFieldValue(ISqlSugarClient db, EntityInfo entityInfo, SplitType splitType, object entityValue) { var splitColumn = entityInfo.Columns.FirstOrDefault(x => x.PropertyInfo.GetCustomAttribute<SplitFieldAttribute>() != null); var value = splitColumn.PropertyInfo.GetValue(entityValue, null); return value; } public string GetTableName(ISqlSugarClient db, EntityInfo EntityInfo) { throw new NotImplementedException(); } public string GetTableName(ISqlSugarClient db, EntityInfo EntityInfo, SplitType type) { throw new NotImplementedException(); } public string GetTableName(ISqlSugarClient db, EntityInfo entityInfo, SplitType splitType, object fieldValue) { return entityInfo.DbTableName + "_" + fieldValue; } }
在GetTableName
中返回最终生成分表名的规则.
对于有自动分表功能的表增删改查的过程中需要联表支持,需要在IUpdateable
、IDeleteable
、IInsertable
、ISugarQueryable
调用方法SplitTable(Func<List<SplitTableInfo>, IEnumerable<SplitTableInfo>> getTableNamesFunc)
6、验证自动分表
当前数据库中不存在workinfo_自动生成测试
的表,调用接口测试
使用Navicat查看数据库中表
根据新增的数据
查看并验证数据
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗