几个月前换了新工作,从技术负责人的岗位上下来,继续回归码农写代码,在新公司中,我不是技术负责人,没太多的话语权。
公司这边项目统一都是使用了SqlSguar这个orm,我也跟着使用了几个月,期间碰见了不少奇奇怪怪的问题,甚至之前特意写文章“骂”过,但是今天要聊的这个问题,至今快月余,依旧让我记忆深刻,以至于控制不住自己要再写一篇文章来聊聊这件事。
准备工作
我准备了这样一张表来进行模拟:
显而易见,address里面存储的是一个json对象。 这张表对应的实体是这样的:
[SugarTable("students", "学生表")]public class Student{[SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]public int Id { get; set; }[SugarColumn(ColumnName = "name")]public string Name { get; set; }[SugarColumn(ColumnName = "address", IsJson = true)]public Address Address { get; set; }}public class Address{public string Province { get; set; }public string City { get; set; }public string Street { get; set; }}
只是作演示使用,所以就没有按照规范写注释,请大家谅解。
有一个Dto对象:
public class StudentDto{public int StudentId { get; set; }public string StudentName { get; set; }public Address StudentAddress { get; set; }}
起因
为了模拟我当时的情况,准备了下面几行代码:
var entity= client.Queryable<Student>().Where(t=>t.Name=="张三").First();Console.WriteLine($"entity:{JsonConvert.SerializeObject(entity)}");var dto = client.Queryable<Student>().Where(t => t.Name == "张三").Select(t => new StudentDto(){StudentId = t.Id,StudentName = t.Name,StudentAddress = t.Address}).First();Console.WriteLine($"dto:{JsonConvert.SerializeObject(dto)}");
然后得到了下面的输出结果:
entity:{"Id":1,"Name":"张三","Address":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}dto:{"StudentId":1,"StudentName":"张三","StudentAddress":null}
Why???? 为什么 dto
的StudentAddress
的值会是NULL
???
我尝试了半个多小时,依旧没有解决这个问题,也没找出原因,最后在万能的群友的帮助下,我找到了解决办法:在dto的StudentAddress属性上,加上这样一个特性标记:[SugarColumn(IsJson = true)]
public class StudentDto{public int StudentId { get; set; }public string StudentName { get; set; }[SugarColumn(IsJson = true)]public Address StudentAddress { get; set; }}
我再试了一下,问题解决:
entity:{"Id":1,"Name":"张三","Address":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}dto:{"StudentId":1,"StudentName":"张三","StudentAddress":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}
问题真的解决了吗?
说实话,这个解决方案我是不太满意的。
首先,StudentDto
是一个dto对象,它的属性为何非要被打上SugarColumn
特性标记?它又不是数据表对应的实体对象。
其次,现在的开发框架体系,无论是多层架构,还是基于DDD模式的那一套,dto对象通常都是单独的一层,(一般命名为 shard或者contract等),这一层不会去引用基础设施层或者是持久化层(Repository),那该如何给dto打上SugarColumn
特性标记?强行引用,就破坏了项目的整体引用结构。我不知道你们是不是能接受,反正我这个中度处女座强迫症洁癖症患者是真接受不了。
最后,这种解决方式,真的很违反直觉。
刨根问底
我打算空闲了,去研究研究Sqlsugar的源码,看看有没有办法优雅的解决掉这个问题。
后面我在群里吹下了牛逼,为了不被打脸,我花了点时间研究源码,好戏正式开始:
调试过程较为繁琐,这里就只展示结果以及部分关键点
开始之前,先把DTO恢复到最开始的样子:
public class StudentDto
{public int StudentId { get; set; }public string StudentName { get; set; }public Address StudentAddress { get; set; }
}
1.釜底抽薪,先找到最后赋值的地方,看看是根据如何进行的数据绑定
经过繁琐的调试,最后找到了关键的地方,在IDataReaderEntityBuilder
文件的第300行,CreateBuilder
方法里面,找到了数据行的处理逻辑。(该文件在 SqlSugar
项目的Abstract\DbBindProvider
文件夹内)
上图有一个非常重要的信息:
sqlguar将dto当作了跟数据表对应的实体类型,并且将其属性包装成了EntityColumnInfo
类型。
上图中那个foreach 循环的源码如下:
foreach (var columnInfo in columnInfos){string fileName = columnInfo.DbColumnName ?? columnInfo.PropertyName;if (columnInfo.IsIgnore && !this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase))){continue;}else if (columnInfo.ForOwnsOnePropertyInfo!=null) {continue;}if (columnInfo != null && columnInfo.PropertyInfo.GetSetMethod(true) != null){var isGemo = columnInfo.PropertyInfo?.PropertyType?.FullName=="NetTopologySuite.Geometries.Geometry";if (!isGemo&&columnInfo.PropertyInfo.PropertyType.IsClass() && columnInfo.PropertyInfo.PropertyType != UtilConstants.ByteArrayType && columnInfo.PropertyInfo.PropertyType != UtilConstants.ObjType){if (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase))){BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));}else if (this.ReaderKeys.Any(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase))){BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)));}}else if (!isGemo && columnInfo.IsJson && columnInfo.PropertyInfo.PropertyType != UtilConstants.StringType){ //json is structif (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase))){BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));}}else{if (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase))){BindField(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));}else if (this.ReaderKeys.Any(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase))){BindField(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)));}}}}
通过这个方法可以看到,依据类型的判断,以及columnInfo
的相关属性判断,来决定究竟是走BindClass()
方法还是BindField()
。经过调试,最终发现StudentAddress
列进入了如下图所示的逻辑分支,并且调用了BindClass()
方法。
继续看看BindClass()
方法的代码:
private void BindClass(ILGenerator generator, LocalBuilder result, EntityColumnInfo columnInfo, string fieldName)
{if (columnInfo.SqlParameterDbType is Type){BindCustomFunc(generator, result, columnInfo, fieldName);return;}if (columnInfo.IsJson){MethodInfo jsonMethod = getJson.MakeGenericMethod(columnInfo.PropertyInfo.PropertyType);int i = DataRecord.GetOrdinal(fieldName);Label endIfLabel = generator.DefineLabel();generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);generator.Emit(OpCodes.Callvirt, isDBNullMethod);generator.Emit(OpCodes.Brtrue, endIfLabel);generator.Emit(OpCodes.Ldloc, result);generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);generator.Emit(OpCodes.Call, jsonMethod);generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));generator.MarkLabel(endIfLabel);}if (columnInfo.IsArray){MethodInfo arrayMehtod = getArray.MakeGenericMethod(columnInfo.PropertyInfo.PropertyType);int i = DataRecord.GetOrdinal(fieldName);Label endIfLabel = generator.DefineLabel();generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);generator.Emit(OpCodes.Callvirt, isDBNullMethod);generator.Emit(OpCodes.Brtrue, endIfLabel);generator.Emit(OpCodes.Ldloc, result);generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);generator.Emit(OpCodes.Call, arrayMehtod);generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));generator.MarkLabel(endIfLabel);}else if (columnInfo.UnderType == typeof(XElement)){int i = DataRecord.GetOrdinal(fieldName);Label endIfLabel = generator.DefineLabel();generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);generator.Emit(OpCodes.Callvirt, isDBNullMethod);generator.Emit(OpCodes.Brtrue, endIfLabel);generator.Emit(OpCodes.Ldloc, result);generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldc_I4, i);BindMethod(generator, columnInfo, i);generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));generator.MarkLabel(endIfLabel);}
}
这个方法就非常的直白明了了: 就是根据columnInfo
的几个属性进行判断,来决定使用不同的数据绑定方式。 ,而且也不难看出,如果columnInfo.IsJson==true
,那么应该就能实现我要效果。
2.看见曙光,直接釜底抽薪
总结一下上面的结论:
- sqlguar将dto当作了跟数据表对应的实体类型,并且将其属性包装成了
EntityColumnInfo
类型。 - 根据
columnInfo
的几个属性进行判断,来决定使用不同的数据绑定方式。
所以不难猜出,使用[SugarColumn(IsJson = true)]
对dto的属性进行修饰,最终应该就是用在了BindClass()
方法里的if (columnInfo.IsJson)
判断上,根据这个决定数据绑定方式。
那么,做一个大胆的假设:如果不使用[SugarColumn(IsJson = true)]
,但是想办法在让它的IsJson
属性变成true,问题是不是就完美解决了?
说干就干, 要给其赋值,首先要明白将dto的属性包装成EntityColumnInfo
究竟发生在哪,它的IsJson
属性又是如何确定值得。
于是又进入了漫长得源码调试阶段。省略其中的繁琐,我们直接看关键部分:
这个方法,核心就是将dto类,包裹成了EntityInfo
类,并且在最下方的SetColumns(result)
方法,对column
进行了设置。继续去看这个方法的代码:
private void SetColumns(EntityInfo result)
{foreach (var property in result.Type.GetProperties()){EntityColumnInfo column = new EntityColumnInfo();//省略部分代码var sugarColumn = property.GetCustomAttributes(typeof(SugarColumn), true).Where(it => it is SugarColumn).Select(it => (SugarColumn)it).FirstOrDefault();//省略部分代码if (sugarColumn?.IsOwnsOne==true){SetValueObjectColumns(result, property, column);}if (sugarColumn.IsNullOrEmpty()){column.DbColumnName = property.Name;}else{if (sugarColumn.IsIgnore == false){//这里就是对各种属性进行赋值,省略部分代码column.IsJson = sugarColumn.IsJson;//省略}else{//。。。}}result.Columns.Add(column);}
}
从上述代码可以看出,这里就是尝试找到result
的SugarColumn
特性,并且给IsJson
等属性赋值。
上述代码在
EntityMaintenance
类里面,该文件在该文件在SqlSugar
项目的Abstract\EntityMaintenance
文件夹内
3.束手无策
事情到这里,就已经结束了,因为我找不到任何办法,可以绕过SugarColumn特性,而将column的IsJson值设置为true。
而这里的代码,应该是属于整个框架里面的核心代码,其外层调用方法的99+
的引用次数,更是让我不敢妄动。
我也考虑过修改数据绑定的那块逻辑,看看能否不通过判断columnInfo.IsJson
也能实现。但是很可惜,也失败了,因为这里要考量的更多,不光是简单的查询,也要考虑多表join,甚至select时多个 对象属性查询,匿名对象(Select(x=>new{})),sqlFunc实现的子查询等N多种复杂情况。
反思
将查询的对象类包装成EntityInfo
似乎是sqlsugar的框架核心实现,这也导致了如果在Select时想要实现 复杂对象 属性的数据绑定,似乎只能依靠SugarColumn
。
但是我真不敢苟同这样的设计,可是我水平有限,目前确实搞不定这个问题。
朋友们都说,dto上打一个特性标记就能解决了,没必要太上纲上线,框架层次引用的清洁性,真的有那么重要吗?
这个问题,我留给大家回答吧。
最后贴一段代码,调试源码的时候发现的,把我看乐了。