FreeSql icon indicating copy to clipboard operation
FreeSql copied to clipboard

动态操作表解构相关的API

Open hyzx86 opened this issue 2 years ago • 11 comments

简要描述原因

FreeSql 已经可以操作巨多 类型的数据库,但是表结构貌似只能通过预定义的类型来同步 同时注意到 FreeSql 的表信息映射功能,比如 DataBaseInfo, TableInfo ,ColumInfo 这些类型 但都只能读取,不能对数据库直接操作,对于某些需要添加字段,增加表这种操作 是否可以通过 上面提到的这些类型进行抽象一下

Feature 特性

传递一个 TableInfo 就可以更新各种类型数据库的表结构

使用场景

CMS 或工作流中的动态类型字段映射

hyzx86 avatar Mar 31 '22 13:03 hyzx86

以前我的想法,这种操作可以考虑用《动态编译》完成,所以提供了相应的 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 这两个类库。

2881099 avatar Apr 01 '22 01:04 2881099

@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();

2881099 avatar Apr 01 '22 01:04 2881099

做过类似的工作,也尝试按每个业务生成一套与之匹配的数据表。

但是我目前我个人的做法是定义一些通用的大宽表,所有数据使用 AutoMapper 清洗以后存储到各种字段里。

大宽表存储了原始 DTO 的类型,以及使用 JsonMap 存储了序列化数据,方便后期重新清洗这些数据。

其实对于表结构变化比较频繁的,我更倾向于使用 MongoDB 这种文档数据库,或者使用 PostgreSQL 这种对 JSON 数据格式友好的数据库。(捂脸逃

hd2y avatar Apr 01 '22 01:04 hd2y

@2881099 可以试试通过用 TypeDescriptor,ICustomTypeDescriptor,TypeDescriptionProvider,PropertyDescriptor 动态扩展特性.来获取相应特性 做操作

LostAsk avatar Apr 01 '22 01:04 LostAsk

做过类似的工作,也尝试按每个业务生成一套与之匹配的数据表。

但是我目前我个人的做法是定义一些通用的大宽表,所有数据使用 AutoMapper 清洗以后存储到各种字段里。

大宽表存储了原始 DTO 的类型,以及使用 JsonMap 存储了序列化数据,方便后期重新清洗这些数据。

其实对于表结构变化比较频繁的,我更倾向于使用 MongoDB 这种文档数据库,或者使用 PostgreSQL 这种对 JSON 数据格式友好的数据库。(捂脸逃

我是想用在OrchardCore 项目里,它使用的Yessql。。数据都保存在JSON里面,但是对于复杂的关联查询就有点蛋疼 虽然OrchardCore支持Lucene查询,但Lucene也不适合做关联查询 尽管有个ElasticSearch的 分支正在做,但距离正式发布可能遥遥无期

hyzx86 avatar Apr 01 '22 03:04 hyzx86

@2881099 感谢叶老板的详尽说明,我也是在考虑使用Natasha 这种动态编译工具

hyzx86 avatar Apr 01 '22 03:04 hyzx86

用 AsType 指定动态类型的方式,在多租户共用服务时可能会存在过期缓存占用内存。

我根据租户分配命名空间,根据自定义数据结构从继承数据基类生成动态类型,然后使用动态创建的类型调用 CodeFirst.SyncStructure 自动生成表(包括按月分表),操作时 fsql.Select<BaseEntity>().AsType(dynamicType)。

看源码有几个 static ConcurrentDictionary 做缓存,字典 key 是 Type, 虽然租户只在初始化时会调整数据结构,但是长时间不重启服务就会有很多的过时的缓存。

我的方案是每次重新生成新的类型时通过反射获取到缓存手动清除。

看叶老板 @2881099 是否有意在字典缓存这块适配动态编译类型的情况,根据类型全名(命名空间+类名)区分缓存或者有个方法能够一句代码清空Type关联的静态字典缓存。

VicBilibily avatar Apr 23 '22 10:04 VicBilibily

@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 avatar Apr 23 '22 12:04 2881099

感谢 @2881099 提供的实现思路,过几天再尝试实现

VicBilibily avatar Apr 26 '22 01:04 VicBilibily

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();
    }
}

VicBilibily avatar May 04 '22 09:05 VicBilibily

研究了 CS-ScriptNatasha 的实现,整出了动态编译的最小可行实现。 仅需要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("动态编译生成错误");
        }
    }
}

VicBilibily avatar May 26 '22 00:05 VicBilibily

期待。

sgf avatar Dec 02 '22 20:12 sgf