Skip to content

Bug: InsertDict 批量插入时 Guid? null 被污染为 Guid.Empty #2260

Description

@autukill
  1. 环境

FreeSql 版本: 3.5.310 (latest)
数据库: SQLite(推测所有支持批量 INSERT 的数据库均受影响)
.NET 版本: net10.0

  1. 描述

InsertDict 批量插入 List<Dictionary<string, object>> 时,如果第一条记录的 Guid? 属性为 null,后续同属性有值,则第一条的 null 值被错误地赋值为了 Guid.Empty。

  1. 复现步骤
// FreeSql InsertDict Bug: Guid? null 在批量插入中被污染为 Guid.Empty
//
// 环境: FreeSql 3.5.310 + SQLite in-memory
// 表现: 一批字典中,前几条 ParentId=null,后几条 ParentId=实际值,
//       查询结果中 null 被变成 Guid.Empty。
// 期望: null 保持为 null(数据库级别为 NULL)。

using FreeSql;
using FreeSql.DataAnnotations;

// 1. 创建 FreeSql 实例(SQLite 内存库)
var fsql = new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, "Data Source=:memory:")
    .UseAutoSyncStructure(true)
    .Build();

fsql.CodeFirst.SyncStructure<TestMenu>();

// 2. 准备测试数据:混合 null 和非 null 的 ParentId
var rootId    = Guid.NewGuid();
var child1Id  = Guid.NewGuid();
var child2Id  = Guid.NewGuid();

var dicts = new List<Dictionary<string, object>>
{
    new() { ["Id"] = rootId,   ["Label"] = "根目录", ["ParentId"] = null   },  // 第一条: null
    new() { ["Id"] = child1Id, ["Label"] = "子菜单1", ["ParentId"] = rootId },   // 第二条: 有值
    new() { ["Id"] = child2Id, ["Label"] = "子菜单2", ["ParentId"] = rootId },   // 第三条: 有值
};

// 3. 用 InsertDict 批量写入
var rows = fsql.InsertDict(dicts).AsTable("test_menu").ExecuteAffrows();
Console.WriteLine($"批量插入影响行数: {rows}");

// 4. 查询验证
var all = fsql.Select<TestMenu>().ToList();
Console.WriteLine("\n查询结果:");
Console.WriteLine($"| {"Id",-36} | {"Label",-10} | {"ParentId",-36} |");
Console.WriteLine(new string('-', 90));

foreach (var m in all)
{
    var parentStr = m.ParentId?.ToString() ?? "null";
    Console.WriteLine($"| {m.Id} | {m.Label,-10} | {parentStr,-36} |");
}

// 5. 检查问题:第一条记录的 ParentId 应该是 null,实际可能是 Guid.Empty
var root = all.First(m => m.Label == "根目录");
bool hasBug = root.ParentId.HasValue && root.ParentId.Value == Guid.Empty;

Console.WriteLine($"\n--- 结果分析 ---");
Console.WriteLine($"根目录 ParentId: {(root.ParentId.HasValue ? root.ParentId.Value.ToString() : "null")}");
Console.WriteLine($"期望值: null");

if (hasBug)
{
    Console.WriteLine("\n❌ BUG 确认: 第一条字典的 ParentId=null 被污染为 Guid.Empty!");
    Console.WriteLine("   根因: FreeSql InsertDict 在批量处理时,第一条的 Guid? null");
    Console.WriteLine("   被后续有值的 Guid 覆盖为默认值 Guid.Empty。");
}
else if (root.ParentId == null)
{
    Console.WriteLine("\n✅ 表现正确: ParentId 保持为 null。");
}
else
{
    Console.WriteLine($"\n⚠️  其他异常: ParentId = {root.ParentId}");
}

// 6. 验证拆分后的行为
Console.WriteLine("\n--- 拆分验证 ---");

var topDicts = new List<Dictionary<string, object>>
{
    new() { ["Id"] = Guid.NewGuid(), ["Label"] = "单独插入", ["ParentId"] = null },
};

var subDicts = new List<Dictionary<string, object>>
{
    new() { ["Id"] = Guid.NewGuid(), ["Label"] = "子菜单X", ["ParentId"] = rootId },
};

fsql.InsertDict(topDicts).AsTable("test_menu").ExecuteAffrows();
fsql.InsertDict(subDicts).AsTable("test_menu").ExecuteAffrows();

var splittedRoot = fsql.Select<TestMenu>().Where(m => m.Label == "单独插入").ToOne();
Console.WriteLine($"拆分后插入 ParentId: {(splittedRoot.ParentId.HasValue ? splittedRoot.ParentId.Value.ToString() : "null")}");
Console.WriteLine($"期望值: null → {(splittedRoot.ParentId == null ? "✅ 正确" : "❌ 错误")}");

// 定义测试用的表结构(用实体类自动建表)
[Table(Name = "test_menu")]
public class TestMenu
{
    [Column(IsPrimary = true)]
    public Guid Id { get; set; }
    public string Label { get; set; } = "";
    public Guid? ParentId { get; set; }
}
  1. 复现项目
    FreeSqlInsertDictNullBug.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions