几个月前换了新工作,从技术负责人的岗位上下来,继续回归码农写代码,在新公司中,我不是技术负责人,没太多的话语权。
公司这边项目统一都是使用了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 struct
- 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(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上打一个特性标记就能办理了,没必要太上纲上线,框架层次引用的干净性,真的有那么重要吗?
这个问题,我留给大家回答吧。
最后贴一段代码,调试源码的时候发现的,把我看乐了。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |