FreeSql
FreeSql copied to clipboard
动态操作表解构相关的API
简要描述原因
FreeSql 已经可以操作巨多 类型的数据库,但是表结构貌似只能通过预定义的类型来同步 同时注意到 FreeSql 的表信息映射功能,比如 DataBaseInfo, TableInfo ,ColumInfo 这些类型 但都只能读取,不能对数据库直接操作,对于某些需要添加字段,增加表这种操作 是否可以通过 上面提到的这些类型进行抽象一下
Feature 特性
传递一个 TableInfo 就可以更新各种类型数据库的表结构
使用场景
CMS 或工作流中的动态类型字段映射
以前我的想法,这种操作可以考虑用《动态编译》完成,所以提供了相应的 API:
fsql.Select<object>().AsType(编译后的实体类型).ToList();
fsql.Insert<object>().AsType(编译后的实体类型).AppendData(object).ExecuteAffrows();
fsql.Update<object>().AsType(编译后的实体类型).SetSource(object).ExecuteAffrows();
fsql.Delete<object>().AsType(编译后的实体类型).WhereDynamic(object).ExecuteAffrows();
fsql.InsertOrUpdate<object>().AsType(编译后的实体类型)).SetSource(object).ExecuteAffrows();
《动态编译》是什么样的技术?
它可以在程序运行的时候(不是运行之前),编译一段字符串,得到一个运行时的 .dll,再使用反射加载该 dll,从而得到对应的功能。
比如计算器:
string exp = "1*2+3/(10-1)";
var dll = 编译代码(@"
public class ExpClass
{
public static object Calc(object input)
{
return input;
}
}
");
var method = dll.GetType("ExpClass").GetMethod("Calc");
var result = method.Invoke(null, new object[] { exp });
Console.WriteLine(result);
使用《动态编译》开发 CMS 或 工作流中的动态表单,可以预先拼接好【实体类型】对应的 c# 代码,这段代码包括 TableAttribute、ColumnAttribute、导航属性等等定义,它和我们程序 .cs 实体代码内容一样,只不过它是一个 string 变量。
《动态技术》可以参考 Natasha、CSScript.Core 这两个类库。
@hyzx86 DataBaseInfo, TableInfo ,ColumInfo 虽然有提供这些,但是这些零碎片段的对象,直接增加复杂度,并且可能发生难以控制的错误。
而实体类型大家已经懂的东西,再配合《动态编译》技术,使用成本会更低。
var dll = 编译代码(@"
[Table(""cms_001"")]
public class YourCMS001
{
[Column(IsIdentity = true)]
public int Id { get; set; }
public int Sex { get; set; }
//todo..
}
");
Type entityType = dll.GetType("YourCMS001");
object entityValue = XxxHelper.DictToEntity(new Dictionary<string, object>
{
["Id"] = 1,
["Sex"] = 1
});
fsql.Insert<object>().AsType(entityType).AppendData(entityValue).ExecuteAffrows();
List<object> result = fsql.Select<object>().AsType(entityType).ToList();
做过类似的工作,也尝试按每个业务生成一套与之匹配的数据表。
但是我目前我个人的做法是定义一些通用的大宽表,所有数据使用 AutoMapper 清洗以后存储到各种字段里。
大宽表存储了原始 DTO 的类型,以及使用 JsonMap 存储了序列化数据,方便后期重新清洗这些数据。
其实对于表结构变化比较频繁的,我更倾向于使用 MongoDB 这种文档数据库,或者使用 PostgreSQL 这种对 JSON 数据格式友好的数据库。(捂脸逃
@2881099 可以试试通过用 TypeDescriptor,ICustomTypeDescriptor,TypeDescriptionProvider,PropertyDescriptor 动态扩展特性.来获取相应特性 做操作
做过类似的工作,也尝试按每个业务生成一套与之匹配的数据表。
但是我目前我个人的做法是定义一些通用的大宽表,所有数据使用 AutoMapper 清洗以后存储到各种字段里。
大宽表存储了原始 DTO 的类型,以及使用 JsonMap 存储了序列化数据,方便后期重新清洗这些数据。
其实对于表结构变化比较频繁的,我更倾向于使用 MongoDB 这种文档数据库,或者使用 PostgreSQL 这种对 JSON 数据格式友好的数据库。(捂脸逃
我是想用在OrchardCore 项目里,它使用的Yessql。。数据都保存在JSON里面,但是对于复杂的关联查询就有点蛋疼 虽然OrchardCore支持Lucene查询,但Lucene也不适合做关联查询 尽管有个ElasticSearch的 分支正在做,但距离正式发布可能遥遥无期
@2881099 感谢叶老板的详尽说明,我也是在考虑使用Natasha 这种动态编译工具
用 AsType 指定动态类型的方式,在多租户共用服务时可能会存在过期缓存占用内存。
我根据租户分配命名空间,根据自定义数据结构从继承数据基类生成动态类型,然后使用动态创建的类型调用 CodeFirst.SyncStructure 自动生成表(包括按月分表),操作时 fsql.Select<BaseEntity>().AsType(dynamicType)。
看源码有几个 static ConcurrentDictionary 做缓存,字典 key 是 Type, 虽然租户只在初始化时会调整数据结构,但是长时间不重启服务就会有很多的过时的缓存。
我的方案是每次重新生成新的类型时通过反射获取到缓存手动清除。
看叶老板 @2881099 是否有意在字典缓存这块适配动态编译类型的情况,根据类型全名(命名空间+类名)区分缓存或者有个方法能够一句代码清空Type关联的静态字典缓存。
@VicBilibily 因为交叉引用关系较复杂,彻底清楚比较困难。
比如:
static ConcurrentDictionary<ColumnInfo, Func<object, object>> _dicGetMapValue;
清除 Type 之前也要对它进行处理。如果我这边提供该功能,假设日后新功能开发又会增加一个新的 ConcurrentDictionary,可能就会遗漏它的清除工作。
我建议使用反射针对性的处理,以下代码可以扫描得到对应的 ConcurrentDictionary:
提醒:程序运行中,清除这些信息有可能影响到正在执行的程序,清除时应锁定整个应用不给执行任务操作。
int LocalConcurrentDictionaryIsTypeKey(Type dictType, int level = 1)
{
if (dictType.IsGenericType == false) return 0;
if (dictType.GetGenericTypeDefinition() != typeof(ConcurrentDictionary<,>)) return 0;
var typeargs = dictType.GetGenericArguments();
if (typeargs[0] == typeof(Type) || typeargs[0] == typeof(ColumnInfo) || typeargs[0] == typeof(TableInfo)) return level;
if (level > 2) return 0;
return LocalConcurrentDictionaryIsTypeKey(typeargs[1], level + 1);
}
var freesqlDLLConcurrentDictionarys = typeof(IFreeSql).Assembly.GetTypes().Select(a => new
{
Type = a,
ConcurrentDictionarys = a.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
.Where(b => LocalConcurrentDictionaryIsTypeKey(b.FieldType) > 0).ToArray()
}).Where(a => a.ConcurrentDictionarys.Length > 0).ToArray();
var freesqlDbContextDLLConcurrentDictionarys = typeof(IBaseRepository).Assembly.GetTypes().Select(a => new
{
Type = a,
ConcurrentDictionarys = a.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
.Where(b => LocalConcurrentDictionaryIsTypeKey(b.FieldType) > 0).ToArray()
}).Where(a => a.ConcurrentDictionarys.Length > 0).ToArray();
//以及其他 FreeSql 相关的 dll 也一并查询后,再清除
感谢 @2881099 提供的实现思路,过几天再尝试实现
original: 实现了一下根据类型全名删除缓存的方法,贴一下代码 2022/05/09: append: 增加 Natasha 动态类构建示例
public Type BuildClass()
{
var nClass = NClass.CreateDomain($"DNC{FreeUtil.NewMongodbId():N}",
opts => opts.CompileWithAssemblyLoadBehavior(LoadBehaviorEnum.UseDefault));
nClass.LoadDomainUsing().NoGlobalUsing().Using<GuidEntity>();
nClass.Namespace("DynamicCustomType").Public();
if (this.TypeId > 0) nClass.Name($"Custom{this.TableType}Table{this.TypeId}");
else nClass.Name($"System{this.TableType}Table{Math.Abs(this.TypeId)}");
nClass.AttributeAppend<DescriptionAttribute>($"\"{this.TableTitle}\"");
nClass.InheritanceAppend<HeadGuidEntity>();
nClass.AttributeAppend<TableAttribute>($"Name = \"cmt_{this.TableName}_{{yyyyMM}}\", AsTable = \"{nameof(GuidEntity.CreateTime)}=2022-1-1(1 month)\"");
nClass!.Property(prop =>
{
prop.Public().Type(field.DataType).Name(field.FieldName!)
.AttributeAppend<DescriptionAttribute>($"\"{field.FieldTitle}\"");
});
var classScript = nClass.GetScript();
return nClass.GetType();
}
2022/05/09: modify: 还有两个实例缓存,加上再贴一下 2022/05/26: mark: fsql.RemoveTypeCache(type) 时 fsql 的实例如果是开启事务后的 uow.Orm 实例会在清空实例缓存时获取不到对应的字典导致报错,建议使用原始的 fsql 实例执行操作,否则需要手动修改一下代码处理报错段。
using FreeSql.Internal;
using FreeSql.Internal.CommonProvider;
using FreeSql.Internal.Model;
using System.Collections;
using System.Collections.Concurrent;
using System.Reflection;
static class FreeSqlStaticCacheExtension
{
static Type conDictType = typeof(ConcurrentDictionary<,>);
static int LocalConcurrentDictionaryIsTypeKey(Type dictType, int level = 1)
{
if (dictType.IsGenericType == false) return 0;
if (dictType.GetGenericTypeDefinition() != conDictType) return 0;
var typeargs = dictType.GetGenericArguments();
if (typeargs[0] == typeof(Type) || typeargs[0] == typeof(ColumnInfo) || typeargs[0] == typeof(TableInfo)) return level;
if (level > 2) return 0;
return LocalConcurrentDictionaryIsTypeKey(typeargs[1], level + 1);
}
static FieldInfo[] FreeSqlDLLInstanceConcurrentDictionarys<Type>()
{
var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
var staticFields = typeof(Type).Assembly.GetTypes()
.SelectMany(a => a.GetFields(bindingFlags)
.Where(b => LocalConcurrentDictionaryIsTypeKey(b.FieldType) > 0))
.ToArray();
return staticFields;
}
static FieldInfo[] FreeSqlDLLStaticConcurrentDictionarys<Type>()
{
var bindingFlags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;
var staticFields = typeof(Type).Assembly.GetTypes()
.SelectMany(a => a.GetFields(bindingFlags)
.Where(b => LocalConcurrentDictionaryIsTypeKey(b.FieldType) > 0))
.ToArray();
staticFields = staticFields.Select(field =>
{
// 静态字段的父类为泛型类型时需要提供初始类型才能通过 GetValue 取到静态字段的值
if (field.DeclaringType is { ContainsGenericParameters: true })
return field.DeclaringType
.MakeGenericType(field.DeclaringType.GetTypeInfo()
.GenericTypeParameters.Select(v => typeof(object)).ToArray())!
.GetField(field.Name, bindingFlags)!;
return field;
}).ToArray();
return staticFields;
}
static Lazy<FieldInfo[]> freeSqlDLLInstanceConcurrentDictionarys = new Lazy<FieldInfo[]>(() => FreeSqlDLLInstanceConcurrentDictionarys<IFreeSql>());
static Lazy<FieldInfo[]> freeSqlDLLConcurrentDictionarys = new Lazy<FieldInfo[]>(() => FreeSqlDLLStaticConcurrentDictionarys<IFreeSql>());
static Lazy<FieldInfo[]> freesqlDbContextDLLConcurrentDictionarys = new Lazy<FieldInfo[]>(() => FreeSqlDLLStaticConcurrentDictionarys<IBaseRepository>());
static IEnumerable<Type> PrivateRemoveDict(IDictionary dic, Type? type)
{
ArgumentNullException.ThrowIfNull(dic, nameof(dic));
ArgumentNullException.ThrowIfNull(type, nameof(type));
switch (dic.Keys)
{
case IEnumerable<Type> types:
foreach (var curr in types.Where(t => t != type && t.FullName == type.FullName))
{
dic.Remove(curr);
yield return curr;
}
break;
case IEnumerable<ColumnInfo> infos:
foreach (var info in infos.Where(i => i.CsType != type && i.CsType.FullName == type.FullName))
{
dic.Remove(info);
yield return info.CsType;
}
break;
case IEnumerable<TableInfo> infos:
foreach (var info in infos.Where(i => i.Type != type && i.Type.FullName == type.FullName))
{
dic.Remove(info);
yield return info.Type;
}
break;
default: throw new NotSupportedException("存在未被实现的缓存清除处理");
}
}
static IEnumerable<Type> RemoveCacheType(FieldInfo[] fields, Type? type)
{
List<Type> rets = new List<Type>();
foreach (var field in fields)
{
if (field.FieldType.GetGenericTypeDefinition() == conDictType)
{
var cacheDict = field.GetValue(null) as IDictionary;
if (cacheDict is not null)
switch (cacheDict.Keys)
{
case IEnumerable<Type>:
case IEnumerable<ColumnInfo>:
case IEnumerable<TableInfo>:
rets.AddRange(PrivateRemoveDict(cacheDict, type));
break;
case IEnumerable<DataType>:
case IEnumerable<string>:
var valType = field.FieldType.GenericTypeArguments[1];
if (valType.GetGenericTypeDefinition() == conDictType)
foreach (IDictionary dict in cacheDict.Values)
rets.AddRange(PrivateRemoveDict(dict, type));
break;
default: throw new NotSupportedException("存在未被实现的缓存清除处理");
}
}
}
return rets;
}
public static IEnumerable<Type> RemoveCache(Type? type)
{
if (type is null) return Array.Empty<Type>();
return RemoveCacheType(freeSqlDLLConcurrentDictionarys.Value, type)
.Union(RemoveCacheType(freesqlDbContextDLLConcurrentDictionarys.Value, type));
}
static FieldInfo fi_CommonUtils_dicConfigEntity = typeof(CommonUtils).GetField("dicConfigEntity", BindingFlags.Instance | BindingFlags.NonPublic)!;
public static void RemoveTypeCache(this IFreeSql fsql, Type? type)
{
if (type is null) return;
var types = RemoveCache(type).Distinct().ToList();
foreach (var field in freeSqlDLLInstanceConcurrentDictionarys.Value)
switch (field)
{
case { Name: "_dicSynced", DeclaringType: { FullName: "FreeSql.Internal.CommonProvider.CodeFirstProvider" } }:
types.AddRange(PrivateRemoveDict((fsql.CodeFirst as CodeFirstProvider)!._dicSynced, type));
break;
case { Name: "dicConfigEntity", DeclaringType: { FullName: "FreeSql.Internal.CommonUtils" } }:
types.AddRange(PrivateRemoveDict((IDictionary)fi_CommonUtils_dicConfigEntity.GetValue(fsql.GetType().GetProperty("InternalCommonUtils")!.GetValue(fsql))!, type));
break;
default: throw new NotSupportedException("存在未被实现的缓存清除处理");
}
// 这里是 Natasha 释放域操作
foreach (var ctype in types.ToHashSet())
{
if (ctype == type) continue;
ctype.DisposeDomain();
}
GC.Collect();
}
}
研究了 CS-Script
和 Natasha
的实现,整出了动态编译的最小可行实现。
仅需要NuGet引用的一个包 Microsoft.CodeAnalysis.CSharp
即可。
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.Loader;
using System.Text;
public partial class DataProvider
{
/// <summary> 实现NET6发布使用单文件生成后的内存元数据引用获取 </summary>
unsafe static PortableExecutableReference LoadAssemblyMetadataReference(Assembly assembly)
{
if (assembly.TryGetRawMetadata(out var blob, out var length))
return AssemblyMetadata.Create(ModuleMetadata.CreateFromMetadata((IntPtr)blob, length)).GetReference();
throw new NotSupportedException();
}
static PortableExecutableReference AutoLoadPortableExecutableReference(Assembly assembly)
=> assembly is { Location.Length: > 0 } ?
MetadataReference.CreateFromFile(assembly.Location) :
LoadAssemblyMetadataReference(assembly);
static readonly Lazy<List<PortableExecutableReference>> MetadataReferences = new Lazy<List<PortableExecutableReference>>(() =>
AppDomain.CurrentDomain.GetAssemblies()
.Where(asm => !asm.GetType().FullName!.EndsWith("AssemblyBuilder"))
.Where(asm => asm is { Location.Length: > 0 })
.Union(new[] { typeof(IFreeSql).Assembly })
.Select(AutoLoadPortableExecutableReference).ToList());
static readonly CSharpCompilationOptions CSharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
Type CompileCodeType(string csscript)
{
var tree = CSharpSyntaxTree.ParseText(csscript);
//tree = CSharpSyntaxTree.ParseText(tree.GetRoot().NormalizeWhitespace().SyntaxTree.ToString());
return CompileCodeType(tree);
}
Type CompileCodeType(Stream csstream)
{
var tree = CSharpSyntaxTree.ParseText(SourceText.From(csstream, Encoding.UTF8));
return CompileCodeType(tree);
}
Type CompileCodeType(SyntaxTree tree)
{
var compilation = CSharpCompilation.Create($"N{Guid.NewGuid():N}", new[] { tree }, MetadataReferences.Value, CSharpCompilationOptions);
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms);
if (result.Success)
{
// 将命名的程序集加载域设置为可回收,当动态生成输出的类型没有引用时GC会回收内存
var alc = new AssemblyLoadContext($"N{FreeUtil.NewMongodbId():N}", true);
ms.Seek(0, SeekOrigin.Begin);
alc.LoadFromStream(ms);
return alc.Assemblies.First().ExportedTypes.First();
}
throw new BadImageFormatException("动态编译生成错误");
}
}
}
期待。