dotnetcore / FreeSql

🦄 .NET aot orm, C# orm, VB.NET orm, Mysql orm, Postgresql orm, SqlServer orm, Oracle orm, Sqlite orm, Firebird orm, 达梦 orm, 人大金仓 orm, 神通 orm, 翰高 orm, 南大通用 orm, 虚谷 orm, 国产 orm, Clickhouse orm, QuestDB orm, MsAccess orm.
http://freesql.net
MIT License
3.99k stars 842 forks source link

仓储层,进行先缓存后 更新的操作的时候. 如果实体字段值是null,会导致识别失败,导致数据库的值被清空. #1746

Closed easy999000 closed 1 month ago

easy999000 commented 2 months ago

问题描述及重现代码:

重大BUG

今天我在使用仓储功能进行,统计数值的更新的时候, 发现数据内部的全部字符串字段被清空, 相当于删库跑路了.

我的操作流程是,先从数据库里面把统计数据查询出来,

然后循环把这个要更新的数据源,附加到仓储模块的缓存中(Attach).
在缓存之前,把统计字段(MaxQuantity)设置成0.缓存完成后,再给改回来. 这样这个要更新的数据源和仓储模块中的缓存.应该只有这一个统计字段(MaxQuantity)的值是不一样的.当然只会更新这一个字段的值.

但是我操作完成之后,发现数据库对应的整条数据,文字内容全被清空了.

经过调试我发现,数据源,的被清空的字段,全是null. 但是正常对比字段值是否变化,前后是一样的,应该不会造成数据库值被清空的问题.

我分析的原因是. 数据库的字段,和对应的实体字符串属性是非空类型. [JsonProperty, Column(StringLength = 100, IsNullable = false)]. 但是我手动做这个待更新实体的时候,并没有给所有的字段赋值,我只赋值了,我需要用到的2个字段.id和MaxQuantity. 那么这就导致其他字段在数据源中的值是null. 正常来说这样也不会有问题.

但是,当我把数据attach到仓储层后, 仓储层把原始数据 也就是null的值保留了下来. 但是当我更新数据的时候,执行.repo.Update 的时候,在update的过程中, 数据源中的null值,被修改成了 "",空字符串. 这就导致,对比数据字段变化的时候. 所有的null值的字段都发生了变化,就导致了异常数据的覆盖.

数据库实体

[JsonObject(MemberSerialization.OptIn), Table(DisableSyncStructure = true)]
public partial class ProjectItem {
    [JsonProperty, Column(DbType = "bigint", IsPrimary = true, IsIdentity = true)]
    public  long  ID { get; set; }

    /// <summary>
    /// 辅材单价
    /// </summary>
    [JsonProperty, Column(DbType = "decimal(14,4)")]
    public  decimal  AuxiliaryUnitPrice { get; set; } = 0.0000M;

    /// <summary>
    /// 编码
    /// </summary>
    [JsonProperty, Column(StringLength = 100, IsNullable = false)]
    public  string  Code { get; set; }

    /// <summary>
    /// 实际最大用量
    /// </summary>
    [JsonProperty, Column(DbType = "decimal(14,4)")]
    public  decimal  MaxQuantity { get; set; } = 0.0000M;

    /// <summary>
    /// 名称
    /// </summary>
    [JsonProperty, Column(StringLength = 50, IsNullable = false)]
    public  string  Name { get; set; }
}

数据库操作方法,也就是这个方法导致的数据被覆盖.

 private void UpdateProjectItemQuantity(int LaborInfoID)
      {
          var ProjectItemIDList = SqlHelper.Select<LaborItem>()
                 .Where(w => w.InfoID == LaborInfoID)
                 .ToList(s => s.ProjectItemID);

          var dbItemMaterialList2 = SqlHelper.Select<LaborItem, LaborInfo>()
              .InnerJoin(j => j.t1.InfoID == j.t2.ID)
               .Where(w => ProjectItemIDList.Contains(w.t1.ProjectItemID))
               //  .Where(w => w.t2.Status == 7)
               .GroupBy(g => g.t1.ProjectItemID)
               .ToList(s => new ProjectItem
               {
                   ID = s.Key,
                   MaxQuantity = s.Sum(s.Value.Item1.Quantity),
                   Code = "" 

               });

          var repo = SqlHelper.GetRepository<ProjectItem>();

          //var updateData = repo.Where(w=> ItemMaterialIDList.Contains(w.ID)).ToList();

          foreach (var item in dbItemMaterialList2)
          {
              var MaxQuantity = item.MaxQuantity;
              item.MaxQuantity = 0;
              repo.Attach(item);
              item.MaxQuantity = MaxQuantity;
          }

          var n1 = repo.Update(dbItemMaterialList2);

      }

图片传不上来,都发在群里了.

数据库版本

mysql 8

安装的Nuget包

.net framework/. net core? 及具体版本

easy999000 commented 2 months ago

ConsoleApp1.zip 测试代码,可以复现.

densen2014 commented 2 months ago

参考测试, 尽量提供开箱即用最小复现代码, 节省时间

ConsoleApp1.zip

image

densen2014 commented 2 months ago

关于可空的问题

ConsoleApp1.zip

LKDI4 45BM ZDS KD2N1~VU

easy999000 commented 2 months ago

经过不断的排除,终于定位具体的bug点位. 当freesql的.Aop.AuditValue.事件挂上一个监控事件后.哪怕是挂一个空方法.都会导致.实体null的字段对比失败. 下面附上,测试代码. 只要.Aop.AuditValue事件有赋值,仓储功能变不能正确对比null字段. 但是奇怪的事情是,2个独立的freesql实体, 既然会互相影响. 这部分代码也在下面的复现代码里面.

ConsoleApp1_2.zip

luoyunchong commented 2 months ago

在AuditValue 逻辑中

https://github.com/dotnetcore/FreeSql/blob/1f7e97869948d2083c721085cba2468190c8fff8/FreeSql/Internal/CommonProvider/UpdateProvider.cs#L531

如果遇到了设置了 IsNullable = false的逻辑,如果数据是null,会主动更新成“”,这样可以避免NULL值数据无法插入的问题

但使用仓储的基于变化的Attach,会把NULL改成“”,会把数据更新成"",原数据库中的值会被清空。

    [JsonProperty, Column(StringLength = 100, IsNullable = false)]
    public  string?  Code { get; set; }
   var t1 = new ProjectItem { ID = 53, MaxQuantity = 0};
   var repo2 = fsql2.GetRepository<ProjectItem>();
   repo2.Attach(t1);
   t1.MaxQuantity = 111;
   var n2 = repo2.Update(t1);

生成的SQL是

UPDATE "ProjectItem" SET "Code" = @p_0, "MaxQuantity" = @p_1, "Name" = @p_2  WHERE ("ID" = 53)

基于仓储的Attach,正确的SQL应该是

UPDATE "ProjectItem" SET "MaxQuantity" = @p_1 WHERE ("ID" = 53)
easy999000 commented 2 months ago

7N2$G4SM_$UVY6XL7J4PNGX 只要这段代码,更新超过1个字段,那就是bug 我的意图很明显,code的值前后是一致的,没有改变 不需要更新. 但是现在会把code给清空. 内部代码不了解,但是现在发现和 .Aop.AuditValue的事件.有关系.

easy999000 commented 2 months ago

在AuditValue 逻辑中

https://github.com/dotnetcore/FreeSql/blob/1f7e97869948d2083c721085cba2468190c8fff8/FreeSql/Internal/CommonProvider/UpdateProvider.cs#L531

如果遇到了设置了 IsNullable = false的逻辑,如果数据是null,会主动更新成“”,这样可以避免NULL值数据无法插入的问题

但使用仓储的基于变化的Attach,会把NULL改成“”,会把数据更新成"",原数据库中的值会被清空。

  [JsonProperty, Column(StringLength = 100, IsNullable = false)]
  public  string?  Code { get; set; }
   var t1 = new ProjectItem { ID = 53, MaxQuantity = 0};
   var repo2 = fsql2.GetRepository<ProjectItem>();
   repo2.Attach(t1);
   t1.MaxQuantity = 111;
   var n2 = repo2.Update(t1);

生成的SQL是

UPDATE "ProjectItem" SET "Code" = @p_0, "MaxQuantity" = @p_1, "Name" = @p_2  WHERE ("ID" = 53)

基于仓储的Attach,正确的SQL应该是

UPDATE "ProjectItem" SET "MaxQuantity" = @p_1 WHERE ("ID" = 53)

有道理.