勿忘初心做自己 发表于 2025-1-13 21:35:48

浅究一下Freesql对Json列的实现

前几天发了一篇随笔,聊了一下在利用Sqlsugar的Json列碰见的一些题目,其时对其解决方案并不是很满意。
今天没啥使命,突发奇想的想看看Freesql是如何实现的,由于国产ORM,目前就这两者争锋了。
上一篇随笔的传送门:继续聊一聊sqlsugar的一个机制题目
省流总结

sqlsugar的json列在查询时,如果利用了dto映射,那么dto对应的属性上,必须利用进行标记,否则该列无法正确的绑定值。 我对这个解决办法不是很满意,由于dto不是实体,它的属性不应该逼迫要求是利用SugarColumn特性,这有些过于耦合了。
接下来,我们来看看Freesql在这块的表现如何。
去其官方查看文档,发现是支持json列的,需要利用特性进行标记。
先看测试代码:
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)}");

   }


   
   public class Student
   {
         
         public int Id { get; set; }

         
         public string Name { get; set; }

         
         
         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; } //请注意这里没有使用 特性进行标记
   }



   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;
   }
}直接上结果:
https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113181114472-1268588339.png
非常棒!Dto没有利用特性,依然能成功并且正确的绑定值,这完全符合我的盼望,而且也应该就是云云!
调试源码,一探究竟

看到结果后,让我有点兴奋,接下来开始调试源码,看看它是如何实现的。
源码的调试较为繁琐,后面仅贴出一些我以为比较关键的代码。
根据官方文档的示例,利用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 />
/// 当实体类属性为【对象】时,并且标记特性 时,该属性将以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 = 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;
}https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113185533581-1954553851.png
https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113185816088-581168530.png
https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113185741008-522433685.png
此时,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)))
    );
}https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113191042016-1260217087.png
https://img2024.cnblogs.com/blog/1553709/202501/1553709-20250113191522108-655902598.png
总结

总的来说,Freesql基于其核心的表达式树(Expression),利用事件注册,全局缓存了Json列对象的信息,实现了Json列的查询和绑定操作,并且DTO的属性映射也能主动完成,这点照旧非常棒的。
后面有时间,看看能否鉴戒这个思路,去改造一下sqlsugar。
快过年了,提前给大家拜个早年,祝大家生活幸福,工作顺利!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 浅究一下Freesql对Json列的实现