浅究一下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]