继续聊一聊sqlsugar的一个机制问题

news/2025/1/8 22:31:53/文章来源:https://www.cnblogs.com/diamondhusky/p/18650702

几个月前换了新工作,从技术负责人的岗位上下来,继续回归码农写代码,在新公司中,我不是技术负责人,没太多的话语权。

公司这边项目统一都是使用了SqlSguar这个orm,我也跟着使用了几个月,期间碰见了不少奇奇怪怪的问题,甚至之前特意写文章“骂”过,但是今天要聊的这个问题,至今快月余,依旧让我记忆深刻,以至于控制不住自己要再写一篇文章来聊聊这件事。

 

 

准备工作

我准备了这样一张表来进行模拟:

image

显而易见,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???? 为什么 dtoStudentAddress的值会是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文件夹内)

image

上图有一个非常重要的信息:
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()方法。
image
继续看看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.看见曙光,直接釜底抽薪

总结一下上面的结论:

  1. sqlguar将dto当作了跟数据表对应的实体类型,并且将其属性包装成了EntityColumnInfo类型。
  2. 根据columnInfo的几个属性进行判断,来决定使用不同的数据绑定方式。

所以不难猜出,使用[SugarColumn(IsJson = true)]对dto的属性进行修饰,最终应该就是用在了BindClass()方法里的if (columnInfo.IsJson)判断上,根据这个决定数据绑定方式。

那么,做一个大胆的假设:如果不使用[SugarColumn(IsJson = true)],但是想办法在让它的IsJson属性变成true,问题是不是就完美解决了?

说干就干, 要给其赋值,首先要明白将dto的属性包装成EntityColumnInfo究竟发生在哪,它的IsJson属性又是如何确定值得。
于是又进入了漫长得源码调试阶段。省略其中的繁琐,我们直接看关键部分:

image

这个方法,核心就是将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);}
}

从上述代码可以看出,这里就是尝试找到resultSugarColumn特性,并且给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上打一个特性标记就能解决了,没必要太上纲上线,框架层次引用的清洁性,真的有那么重要吗?

这个问题,我留给大家回答吧。

最后贴一段代码,调试源码的时候发现的,把我看乐了。

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/863495.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

高性能MySQL(第4版)PDF、EPUB免费下载

领域经典十年后全版更新||全面拥抱8.0||重磅剖析现代云数据库与大规模运维实践||中国首批DBA精琢翻译5大头部国产数据库创始人联合力荐适读人群 :不但适合数据库管理员(DBA)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获 领域经典十年后…

【校长系列】2022版高中信息技术课程标准详细解读(PPT课件)

【校长系列】2022版高中信息技术课程标准详细解读(PPT课件)新课标解读

采集DNP3 转 profinet IO项目案例

VFBOX协议转换网关支持PLC,modbus,EthernetIP,Profinet,CCLink,EtherCAT,IEC61850,IEC104,bacnet,DLT645,HJ212,opc ua,opc da,DNP3。目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 准备工作 2 4 配置网关采集DNP3协议数据 2 5 用PROFINET IO协议转发数据 4 6 案例总…

PPT保存报错:某些字体无法与演示文稿一起保存.241127

WPS做的PPT,用的会员,高级的那种,很高大上很好看,但是用微软的PowerPoint编辑保存,报错:某些字体无法与演示文稿一起保存。 解决方案:移除嵌入的字体 打开演示文稿文件。 在“文件”菜单上选择“选项”。 选择对话框左侧的“保存”选项卡。 在“共享此演示文稿时保持保真…

API开发与管理规范v1.0_.20241127

1. 协议规范 为了确保不同业务系统之间以及前后端的的数据交互的快捷性,通讯协议统一约定如下:对内调用的API接口统一使用 HTTP协议 对外互联网发布的API建议使用HTTPS协议也可以使用HTTP 新的API接口必须使用标准的HTTP报文并使用JSON作为统一的数据传送标准 如无特殊情况禁…

Node开发规范v1.0_.20241127

一、空格与格式 (一)缩进 采用2个空格缩进,而不是tab缩进。 空格在编辑器中与字符是等宽的,而tab可能因编辑器的设置不同。2个空格会让代码看起来更紧凑、明快。 变量声明 永远用var声明变量,不加var时会将其变成全局变量,这样可能会意外污染上下文,或是被意外污染。 在EC…

ABAP开发规范V1.0_.20241127

1. 概要 1.1目的 该文档定义了在开发与维护ABAP程序过程中必须遵守的规范与标准。该文档应当被视为一个动态的文档,该文档会根据需要进行增补和修订。 开发规范的重要作用在于保持整个开发团队的开发风格一致,提高程序质量,降低维护压力。 1.2适用范围 所有ABAP开发及系统配…

浙江单考单招政策解读!(内含浙江单招院校名单)

浙江单考单招政策解读!(内含浙江单招院校名单) 浙江单考单招: 浙江单招,即浙江省普通高校招生单独考试,浙江单招的考试科目通常包括文化课和职业技能测试(分为理论课和操作两部分),很多考生对浙江单招政策还有很多疑问,小编整理了浙江单招政策常见的一些政策问题及单…

如何评估员工是否以客户为中心

达到或超过期望值他能够与客户的反对意见打交道,应对它们,并使客户相信他的观点的优点。 他能为任何客户找到正确的方法,即使是最关键的客户。 他对客户总是很有礼貌和友好。他在谈话中从不打断他们。 他不能让客户失望。如果他承诺过要做什么,他就会履行承诺。 他总是关心…

antdVue 合并a-table相同内容行的方法

{title: "核算项",dataIndex: "hesxName",key: "hesxName",align: "center",customRender: (text, record, index) => {const obj = {children: text !== null ? text : "",attrs: {},};obj.attrs.rowSpan = this.merge…

在jooq的POJO类中使用Lombok的Data注解

jooq生成pojo类的配置根据官方给的如下: https://www.jooq.org/doc/3.14/manual/getting-started/tutorials/jooq-in-7-steps/jooq-in-7-steps-step3/如果想要使用lombok注解,需要自定义生成器。原生的参数并不足够支持这样做。另外一般也不需要这样做,因为更推荐把生成的代…

01.03 CW 模拟赛 T1. math

前言 赛场上 \(\rm{while}\) 打成 \(\rm{if}\) 痛失 \(40 \rm{pts}\) 不过下来看是贪心的话也没什么好做的了, 一般都不会 对了这是题目 题目下载 \(\rm{sol}\) 方法 \(1\) : 逐位计算 思路 显然的是你需要把数字从大到小填入, 使得高位的数尽量大, 这个显然 由上面的结论可以知…