浅究一下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;
}
}
直接上结果:
非常棒!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。
快过年了,提前给大家拜个早年,祝大家生活幸福,工作顺利!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~