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

打印 上一主题 下一主题

主题 835|帖子 835|积分 2505

几个月前换了新工作,从技术负责人的岗位上下来,继续回归码农写代码,在新公司中,我不是技术负责人,没太多的话语权。
公司这边项目统一都是使用了SqlSguar这个orm,我也跟着使用了几个月,期间碰见了不少奇希奇怪的问题,乃至之前特意写文章“骂”过,但是本日要聊的这个问题,至今快月余,依旧让我影象深刻,以至于控制不住自己要再写一篇文章来聊聊这件事。
 
 
准备工作

我准备了如许一张表来进行模拟:

显而易见,address里面存储的是一个json对象。 这张表对应的实体是如许的:
  1.     [SugarTable("students", "学生表")]
  2.     public class Student
  3.     {
  4.         [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]
  5.         public int Id { get; set; }
  6.         [SugarColumn(ColumnName = "name")]
  7.         public string Name { get; set; }
  8.         [SugarColumn(ColumnName = "address", IsJson = true)]
  9.         public Address Address { get; set; }
  10.     }
  11.     public class Address
  12.     {
  13.         public string Province { get; set; }
  14.         public string City { get; set; }
  15.         public string Street { get; set; }
  16.     }
复制代码
只是作演示使用,以是就没有按照规范写注释,请大家谅解。
有一个Dto对象:
  1. public class StudentDto
  2. {
  3.      public int StudentId { get; set; }
  4.      public string StudentName { get; set; }
  5.      public Address StudentAddress { get; set; }
  6. }
复制代码
起因

为了模拟我其时的情况,准备了下面几行代码:
  1.   var entity= client.Queryable<Student>().Where(t=>t.Name=="张三").First();
  2.   Console.WriteLine($"entity:{JsonConvert.SerializeObject(entity)}");
  3.   var dto = client.Queryable<Student>().Where(t => t.Name == "张三").Select(t => new StudentDto()
  4.   {
  5.       StudentId = t.Id,
  6.       StudentName = t.Name,
  7.       StudentAddress = t.Address
  8.   }).First();
  9.   Console.WriteLine($"dto:{JsonConvert.SerializeObject(dto)}");
复制代码
然后得到了下面的输出效果:
  1. entity:{"Id":1,"Name":"张三","Address":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}
  2. dto:{"StudentId":1,"StudentName":"张三","StudentAddress":null}
复制代码
Why???? 为什么 dto的StudentAddress的值会是NULL???
我尝试了半个多小时,依旧没有办理这个问题,也没找出缘故原由,最后在万能的群友的帮助下,我找到了办理办法:在dto的StudentAddress属性上,加上如许一个特性标记:[SugarColumn(IsJson = true)]
  1. public class StudentDto
  2. {
  3.      public int StudentId { get; set; }
  4.      public string StudentName { get; set; }
  5.      [SugarColumn(IsJson = true)]
  6.      public Address StudentAddress { get; set; }
  7. }
复制代码
我再试了一下,问题办理:
  1. entity:{"Id":1,"Name":"张三","Address":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}
  2. dto:{"StudentId":1,"StudentName":"张三","StudentAddress":{"Province":"湖北省","City":"武汉市","Street":"发展大道234号"}}
复制代码
问题真的办理了吗?

说实话,这个办理方案我是不太满意的。
起首,StudentDto是一个dto对象,它的属性为何非要被打上SugarColumn特性标记?它又不是数据表对应的实体对象。
其次,现在的开发框架体系,无论是多层架构,还是基于DDD模式的那一套,dto对象通常都是单独的一层,(一样平常命名为 shard或者contract等),这一层不会去引用底子办法层或者是持久化层(Repository),那该如何给dto打上SugarColumn特性标记?强行引用,就粉碎了项目的整体引用结构。我不知道你们是不是能担当,反正我这个中度童贞座强迫症洁癖症患者是真担当不了。
最后,这种办理方式,真的很违反直觉
刨根问底

我计划空闲了,去研究研究Sqlsugar的源码,看看有没有办法优雅的办理掉这个问题。
后面我在群里吹下了牛逼,为了不被打脸,我花了点时间研究源码,好戏正式开始:
调试过程较为繁琐,这里就只展示效果以及部分关键点
开始之前,先把DTO恢复到最开始的样子:
  1. public class StudentDto
  2. {
  3.     public int StudentId { get; set; }
  4.     public string StudentName { get; set; }
  5.     public Address StudentAddress { get; set; }
  6. }
复制代码
1.釜底抽薪,先找到最后赋值的地方,看看是根据如何进行的数据绑定

颠末繁琐的调试,最后找到了关键的地方,在IDataReaderEntityBuilder文件的第300行,CreateBuilder方法里面,找到了数据行的处理逻辑。(该文件在 SqlSugar项目的Abstract\DbBindProvider文件夹内)

上图有一个非常重要的信息:
sqlguar将dto当作了跟数据表对应的实体类型,而且将其属性包装成了EntityColumnInfo类型。
上图中谁人foreach 循环的源码如下:
  1. foreach (var columnInfo in columnInfos)
  2. {
  3.      string fileName = columnInfo.DbColumnName ?? columnInfo.PropertyName;
  4.      if (columnInfo.IsIgnore && !this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)))
  5.      {
  6.          continue;
  7.      }
  8.      else if (columnInfo.ForOwnsOnePropertyInfo!=null)
  9.      {
  10.          continue;
  11.      }
  12.      if (columnInfo != null && columnInfo.PropertyInfo.GetSetMethod(true) != null)
  13.      {
  14.          var isGemo = columnInfo.PropertyInfo?.PropertyType?.FullName=="NetTopologySuite.Geometries.Geometry";
  15.          if (!isGemo&&columnInfo.PropertyInfo.PropertyType.IsClass() && columnInfo.PropertyInfo.PropertyType != UtilConstants.ByteArrayType && columnInfo.PropertyInfo.PropertyType != UtilConstants.ObjType)
  16.          {
  17.              if (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)))
  18.              {
  19.                  BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));
  20.              }
  21.              else if (this.ReaderKeys.Any(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)))
  22.              {
  23.                  BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)));
  24.              }
  25.          }
  26.          else if (!isGemo && columnInfo.IsJson && columnInfo.PropertyInfo.PropertyType != UtilConstants.StringType)
  27.          {   //json is struct
  28.              if (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)))
  29.              {
  30.                  BindClass(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));
  31.              }
  32.          }
  33.          else
  34.          {
  35.              if (this.ReaderKeys.Any(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)))
  36.              {
  37.                  BindField(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)));
  38.              }
  39.              else if (this.ReaderKeys.Any(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)))
  40.              {
  41.                  BindField(generator, result, columnInfo, ReaderKeys.First(it => it.Equals(columnInfo.PropertyName, StringComparison.CurrentCultureIgnoreCase)));
  42.              }
  43.          }
  44.      }
  45. }
复制代码
通过这个方法可以看到,依据类型的判断,以及columnInfo的相干属性判断,来决定究竟是走BindClass()方法还是BindField()。颠末调试,最终发现StudentAddress列进入了如下图所示的逻辑分支,而且调用了BindClass()方法。

继续看看BindClass()方法的代码:
  1. private void BindClass(ILGenerator generator, LocalBuilder result, EntityColumnInfo columnInfo, string fieldName)
  2. {
  3.     if (columnInfo.SqlParameterDbType is Type)
  4.     {
  5.         BindCustomFunc(generator, result, columnInfo, fieldName);
  6.         return;
  7.     }
  8.     if (columnInfo.IsJson)
  9.     {
  10.         MethodInfo jsonMethod = getJson.MakeGenericMethod(columnInfo.PropertyInfo.PropertyType);
  11.         int i = DataRecord.GetOrdinal(fieldName);
  12.         Label endIfLabel = generator.DefineLabel();
  13.         generator.Emit(OpCodes.Ldarg_0);
  14.         generator.Emit(OpCodes.Ldc_I4, i);
  15.         generator.Emit(OpCodes.Callvirt, isDBNullMethod);
  16.         generator.Emit(OpCodes.Brtrue, endIfLabel);
  17.         generator.Emit(OpCodes.Ldloc, result);
  18.         generator.Emit(OpCodes.Ldarg_0);
  19.         generator.Emit(OpCodes.Ldc_I4, i);
  20.         generator.Emit(OpCodes.Call, jsonMethod);
  21.         generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));
  22.         generator.MarkLabel(endIfLabel);
  23.     }
  24.     if (columnInfo.IsArray)
  25.     {
  26.         MethodInfo arrayMehtod = getArray.MakeGenericMethod(columnInfo.PropertyInfo.PropertyType);
  27.         int i = DataRecord.GetOrdinal(fieldName);
  28.         Label endIfLabel = generator.DefineLabel();
  29.         generator.Emit(OpCodes.Ldarg_0);
  30.         generator.Emit(OpCodes.Ldc_I4, i);
  31.         generator.Emit(OpCodes.Callvirt, isDBNullMethod);
  32.         generator.Emit(OpCodes.Brtrue, endIfLabel);
  33.         generator.Emit(OpCodes.Ldloc, result);
  34.         generator.Emit(OpCodes.Ldarg_0);
  35.         generator.Emit(OpCodes.Ldc_I4, i);
  36.         generator.Emit(OpCodes.Call, arrayMehtod);
  37.         generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));
  38.         generator.MarkLabel(endIfLabel);
  39.     }
  40.     else if (columnInfo.UnderType == typeof(XElement))
  41.     {
  42.         int i = DataRecord.GetOrdinal(fieldName);
  43.         Label endIfLabel = generator.DefineLabel();
  44.         generator.Emit(OpCodes.Ldarg_0);
  45.         generator.Emit(OpCodes.Ldc_I4, i);
  46.         generator.Emit(OpCodes.Callvirt, isDBNullMethod);
  47.         generator.Emit(OpCodes.Brtrue, endIfLabel);
  48.         generator.Emit(OpCodes.Ldloc, result);
  49.         generator.Emit(OpCodes.Ldarg_0);
  50.         generator.Emit(OpCodes.Ldc_I4, i);
  51.         BindMethod(generator, columnInfo, i);
  52.         generator.Emit(OpCodes.Callvirt, columnInfo.PropertyInfo.GetSetMethod(true));
  53.         generator.MarkLabel(endIfLabel);
  54.     }
  55. }
复制代码
这个方法就非常的直白明了了: 就是根据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进行了设置。继续去看这个方法的代码:
  1. private void SetColumns(EntityInfo result)
  2. {
  3.     foreach (var property in result.Type.GetProperties())
  4.     {
  5.         EntityColumnInfo column = new EntityColumnInfo();
  6.         //省略部分代码
  7.         var sugarColumn = property.GetCustomAttributes(typeof(SugarColumn), true)
  8.         .Where(it => it is SugarColumn)
  9.         .Select(it => (SugarColumn)it)
  10.         .FirstOrDefault();
  11.          //省略部分代码
  12.         if (sugarColumn?.IsOwnsOne==true)
  13.         {
  14.             SetValueObjectColumns(result, property, column);
  15.         }
  16.         if (sugarColumn.IsNullOrEmpty())
  17.         {
  18.             column.DbColumnName = property.Name;
  19.         }
  20.         else
  21.         {
  22.             if (sugarColumn.IsIgnore == false)
  23.             {
  24.                 //这里就是对各种属性进行赋值,省略部分代码
  25.                 column.IsJson = sugarColumn.IsJson;
  26.                //省略
  27.             }
  28.             else
  29.             {
  30.                //。。。
  31.             }
  32.         }
  33.         result.Columns.Add(column);
  34.     }
  35. }
复制代码
从上述代码可以看出,这里就是尝试找到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企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

前进之路

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表