前几天发了一篇随笔,聊了一下在利用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;
- }
- }
复制代码 直接上结果:

非常棒!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;
- }
复制代码


此时,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)))
- );
- }
复制代码

总结
总的来说,Freesql基于其核心的表达式树(Expression),利用事件注册,全局缓存了Json列对象的信息,实现了Json列的查询和绑定操作,并且DTO的属性映射也能主动完成,这点照旧非常棒的。
后面有时间,看看能否鉴戒这个思路,去改造一下sqlsugar。
快过年了,提前给大家拜个早年,祝大家生活幸福,工作顺利!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |