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中返回最终生成分表名的规则.

对于有自动分表功能的表增删改查的过程中需要联表支持,需要在IUpdateableIDeleteableIInsertableISugarQueryable调用方法SplitTable(Func<List<SplitTableInfo>, IEnumerable<SplitTableInfo>> getTableNamesFunc)

6、验证自动分表

当前数据库中不存在workinfo_自动生成测试的表,调用接口测试

使用Navicat查看数据库中表

根据新增的数据

查看并验证数据

SqlSugar ORM 5.X 官网 (donet5.com)

posted @   非法关键字  阅读(307)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示