浅究一下Freesql对Json列的实现

前几天发了一篇随笔,聊了一下在使用Sqlsugar的Json列碰见的一些问题,当时对其解决方案并不是很满意。

今天没啥任务,突发奇想的想看看Freesql是如何实现的,因为国产ORM,目前就这两者争锋了。

上一篇随笔的传送门:继续聊一聊sqlsugar的一个机制问题

省流总结

sqlsugar的json列在查询时,如果使用了dto映射,那么dto对应的属性上,必须使用[SugarColumn(IsJson = true)]进行标记,否则该列无法正确的绑定值。 我对这个解决办法不是很满意,因为dto不是实体,它的属性不应该强制要求是使用SugarColumn特性,这有些过于耦合了。

接下来,我们来看看Freesql在这块的表现如何。

去其官方查看文档,发现是支持json列的,需要使用[JsonMap]特性进行标记。

先看测试代码:

 internal class Program
 {
     static void Main(string[] args)
     {

         var freesql = MysqlProvider.Instance;

         freesql.UseJsonMap(); //这行代码是关键

         var entity = freesql.Select<Student>().Where(a => a.Name == "张三").First();

         Console.WriteLine($"entity:{JsonConvert.SerializeObject(entity)}");

         var dto = freesql.Select<Student>().Where(a => a.Name == "张三")
                   .First(x => new StudentDto
                   {
                       StudentAddress = x.Address,
                       StudentId = x.Id,
                       StudentName = x.Name
                   });

         Console.WriteLine($"dto:{JsonConvert.SerializeObject(dto)}");

     }


     [Table(Name = "students")]
     public class Student
     {
         [Column(Name = "id", IsPrimary = true, IsIdentity = true)]
         public int Id { get; set; }

         [Column(Name = "name")]
         public string Name { get; set; }

         [Column(Name = "address")]
         [JsonMap]
         public Address Address { get; set; }
     }

     public class Address
     {
         public string Province { get; set; }

         public string City { get; set; }

         public string Street { get; set; }
     }

     public class StudentDto
     {
         public int StudentId { get; set; }

         public string StudentName { get; set; }

         public Address StudentAddress { get; set; } //请注意这里没有使用 [JsonMap]特性进行标记
     }



     public class MysqlProvider
     {
         private static Lazy<IFreeSql> mysqlLazy = new Lazy<IFreeSql>(() => new FreeSql.FreeSqlBuilder()
      .UseConnectionString(FreeSql.DataType.MySql, "server=127.0.0.1;port=3306;user=root;password=abc123;database=json_test;Charset=utf8;sslMode=None;AllowLoadLocalInfile=true")
      .UseAutoSyncStructure(false)
      .UseMonitorCommand(
          cmd => Trace.WriteLine("\r\n线程" + Thread.CurrentThread.ManagedThreadId + ": " + cmd.CommandText) //监听SQL命令对象,在执行前
                                                                                                           //, (cmd, traceLog) => Console.WriteLine(traceLog)
          )
      .UseLazyLoading(true)
      .Build());
         public static IFreeSql Instance => mysqlLazy.Value;
     }
 }

直接上结果:
image

非常棒!Dto没有使用[JsonMap]特性,依然能成功并且正确的绑定值,这完全符合我的期望,而且也应该就是如此!

调试源码,一探究竟

看到结果后,让我有点兴奋,接下来开始调试源码,看看它是如何实现的。

源码的调试较为繁琐,后面仅贴出一些我认为比较关键的代码。

根据官方文档的示例,使用Json列相关功能前,必须要加上这样一行代码:

fsql.UseJsonMap();

很明显,这个方法里面应该就是核心关键,这个方法的部分代码如下:

/// <summary>
/// When the entity class property is <see cref="object"/> and the attribute is marked as <see cref="JsonMapAttribute"/>, map storage in JSON format. <br />
/// 当实体类属性为【对象】时,并且标记特性 [JsonMap] 时,该属性将以JSON形式映射存储
/// </summary>
/// <returns></returns>
public static void UseJsonMap(this IFreeSql fsql)
{
    UseJsonMap(fsql, JsonConvert.DefaultSettings?.Invoke() ?? new JsonSerializerSettings());
}

public static void UseJsonMap(this IFreeSql fsql, JsonSerializerSettings settings)
{
    if (Interlocked.CompareExchange(ref _isAoped, 1, 0) == 0)
    {
        FreeSql.Internal.Utils.GetDataReaderValueBlockExpressionSwitchTypeFullName.Add((LabelTarget returnTarget, Expression valueExp, Type type) =>
        {
            if (_dicTypes.ContainsKey(type)) return Expression.IfThenElse(
                Expression.TypeIs(valueExp, type),
                Expression.Return(returnTarget, valueExp),
                Expression.Return(returnTarget, Expression.TypeAs(Expression.Call(MethodJsonConvertDeserializeObject, Expression.Convert(valueExp, typeof(string)), Expression.Constant(type)), type))
            );
            return null;
        });
    }
    //关键代码,实体属性配置事件,整个功能实现原理的核心
    fsql.Aop.ConfigEntityProperty += (s, e) =>
    {
        var isJsonMap = e.Property.GetCustomAttributes(typeof(JsonMapAttribute), false).Any() || _dicJsonMapFluentApi.TryGetValue(e.EntityType, out var tryjmfu) && tryjmfu.ContainsKey(e.Property.Name);
        if (isJsonMap)
        {
            if (_dicTypes.ContainsKey(e.Property.PropertyType) == false &&
                FreeSql.Internal.Utils.dicExecuteArrayRowReadClassOrTuple.ContainsKey(e.Property.PropertyType))
                return; //基础类型使用 JsonMap 无效

            if (e.ModifyResult.MapType == null)
            {
                e.ModifyResult.MapType = typeof(string);
                e.ModifyResult.StringLength = -2;
            }
            if (_dicTypes.TryAdd(e.Property.PropertyType, true))
            {
                lock (_concurrentObj)
                {
                    FreeSql.Internal.Utils.dicExecuteArrayRowReadClassOrTuple[e.Property.PropertyType] = true;
                    //关键代码,将json列类型进行了存储。
                    FreeSql.Internal.Utils.GetDataReaderValueBlockExpressionObjectToStringIfThenElse.Add((LabelTarget returnTarget, Expression valueExp, Expression elseExp, Type type) =>
                    {
                        return Expression.IfThenElse(
                            Expression.TypeIs(valueExp, e.Property.PropertyType),
                            Expression.Return(returnTarget, Expression.Call(MethodJsonConvertSerializeObject, Expression.Convert(valueExp, typeof(object)), Expression.Constant(settings)), typeof(object)),
                            elseExp);
                    });
                }
            }
        }
    };
    //省略....
}

从上述代码可以看到,UseJsonMap方法,注册了ConfigEntityProperty事件,如果该属性有JsonMap特性,则将其保存到FreeSql.Internal.Utils.GetDataReaderValueBlockExpressionObjectToStringIfThenElse里面。

那么,在哪里触发的这个事件呢?继续调试...

在Uitls类里面,有一个方法GetTableByEntity,这里遍历实体的属性,然后调用CommonUtils的GetEntityColumnAttribute方法,处理实体属性的PropertyInfo:

internal static TableInfo GetTableByEntity(Type entity, CommonUtils common)
{
    // ....省略
    foreach (var p in trytb.Properties.Values)
    {
        var setMethod = p.GetSetMethod(true); //trytb.Type.GetMethod($"set_{p.Name}");
        var colattr = common.GetEntityColumnAttribute(entity, p); //核心

    }
    //....省略
}
public ColumnAttribute GetEntityColumnAttribute(Type type, PropertyInfo proto)
{
    var attr = new ColumnAttribute();
    foreach (var mp in _mappingPriorityTypes)
    {
        switch (mp)
        {
            case MappingPriorityType.Aop:
                if (_orm.Aop.ConfigEntityPropertyHandler != null)
                {
                    var aope = new Aop.ConfigEntityPropertyEventArgs(type, proto)
                    {
                       //....省略
                    };
                    _orm.Aop.ConfigEntityPropertyHandler(_orm, aope); //这里触发ConfigEntityProperty事件
                    //....省略
                }
                break;
            case MappingPriorityType.FluentApi:
                //省略
                break;
            case MappingPriorityType.Attribute:
                //省略
                break;
        }
    }
    ColumnAttribute ret = null;
    //....省略
    return ret;
}

image

image

image

此时,json列对应的类型信息已经被freesql拿到,那么只需要在赋值的时候,从FreeSql.Internal.Utils.GetDataReaderValueBlockExpressionObjectToStringIfThenElse拿出即可。

经过一系列调试,来到了ReadAnonymous方法,该方法里面调用了GetDataReaderValue的方法,最终调用GetDataReaderValueBlockExpression方法,生成最终的表达式树,然后执行拿到值并且完成Json列的数据绑定:

public static object GetDataReaderValue(Type type, object value)
{
    //if (value == null || value == DBNull.Value) return Activator.CreateInstance(type);
    if (type == null) return value;
    var valueType = value?.GetType() ?? type;
    if (TypeHandlers.TryGetValue(valueType, out var typeHandler)) return typeHandler.Serialize(value);
    var func = _dicGetDataReaderValue.GetOrAdd(type, k1 => new ConcurrentDictionary<Type, Func<object, object>>()).GetOrAdd(valueType, valueType2 =>
    {
        var parmExp = Expression.Parameter(typeof(object), "value");
        var exp = GetDataReaderValueBlockExpression(type, parmExp); //核心代码
        return Expression.Lambda<Func<object, object>>(exp, parmExp).Compile();
    });
    try
    {
        return func(value);
    }
    catch (Exception ex)
    {
        throw new ArgumentException(CoreErrorStrings.ExpressionTree_Convert_Type_Error(string.Concat(value), value.GetType().FullName, type.FullName, ex.Message));
    }
}
public static Expression GetDataReaderValueBlockExpression(Type type, Expression value)
{
    var returnTarget = Expression.Label(typeof(object));
    var valueExp = Expression.Variable(typeof(object), "locvalue");
    Expression LocalFuncGetExpression(bool ignoreArray = false)
    {
        //....省略
        var typeOrg = type;
        if (type.IsNullableType()) type = type.GetGenericArguments().First();
        Expression tryparseExp = null;
        Expression tryparseBooleanExp = null;
        ParameterExpression tryparseVarExp = null;
        ParameterExpression tryparseVarExpDecimal = null;
        switch (type.FullName)
        {
            case "System.Guid":
            case "System.Numerics.BigInteger":
            case "System.TimeOnly": //....省略,下同
            case "System.TimeSpan":
            case "System.DateOnly":
            case "System.DateTime":
            case "System.DateTimeOffset":
            case "System.Char":
            case "System.SByte":
            case "System.Int16":
            case "System.Int32":
            case "System.Int64":
            case "System.Byte":
            case "System.UInt16":
            case "System.UInt32":
            case "System.UInt64":
            case "System.Single":
            case "System.Double":
            case "System.Decimal":
            case "System.Boolean":
            case "System.Collections.BitArray":
            default:
                if (type.IsEnum && TypeHandlers.ContainsKey(type) == false)
                    return Expression.IfThenElse(
                        Expression.Equal(Expression.TypeAs(valueExp, typeof(string)), Expression.Constant(string.Empty)),
                        Expression.Return(returnTarget, Expression.Convert(Expression.Default(type), typeof(object))),
                        Expression.Return(returnTarget, Expression.Call(MethodEnumParse, Expression.Constant(type, typeof(Type)), Expression.Call(MethodToString, valueExp), Expression.Constant(true, typeof(bool))))
                    );
                foreach (var switchFunc in GetDataReaderValueBlockExpressionSwitchTypeFullName)
                {
                    var switchFuncRet = switchFunc(returnTarget, valueExp, type);//拿出最开始存储的Json列相关委托并且执行
                    if (switchFuncRet != null) return switchFuncRet;
                }
                break;
        }
    };

    return Expression.Block(
        new[] { valueExp },
        Expression.Assign(valueExp, Expression.Convert(value, typeof(object))),
        Expression.IfThenElse(
            Expression.OrElse(
                Expression.Equal(valueExp, Expression.Constant(null)),
                Expression.Equal(valueExp, Expression.Constant(DBNull.Value))
            ),
            Expression.Return(returnTarget, Expression.Convert(Expression.Default(type), typeof(object))),
            LocalFuncGetExpression()
        ),
        Expression.Label(returnTarget, Expression.Default(typeof(object)))
    );
}

image

image

总结

总的来说,Freesql基于其核心的表达式树(Expression),利用事件注册,全局缓存了Json列对象的信息,实现了Json列的查询和绑定操作,并且DTO的属性映射也能自动完成,这点还是非常棒的。

后面有时间,看看能否借鉴这个思路,去改造一下sqlsugar。

快过年了,提前给大家拜个早年,祝大家生活幸福,工作顺利!

posted @   呆萌哈士奇  阅读(137)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示