存储和恢复与Dapper相关的数据的最佳方法
#dotnet #database #csharp #dapper

英语? Go

开始对话,我需要留下一些非常透明的东西:我是“懒惰”。用英语,这比翻译更可口,甚至是“懒惰”。知道这一点,我需要透露一个秘密。如果我不喜欢做一件事,就是在SQL中写约会或命令。通常,这些结构在我的富有想象力中都是美丽而奇妙的,当我需要将它们挡住时,带有许多“惠姆”和“加入”时,我已经感到眩晕了。因此,我一直是Chatgpt的非常异性的用户,这通过做无聊的部分对我有很大帮助。
这与本文有什么关系?我开始探索使用C#,Dapper和SQL Server从对象或复杂关系中存储和恢复数据的不同方法,我将描绘这些经验的井摘要。

对象和关系

要开始这些测试,我们需要一些对象,这些对象与彼此之间至少有一种关系,毕竟,这项研究的目标是发现在执行时间内组装这种关系的最有效方法£ o。由于目前在我的日常生活中,我在财务部门工作很多,我建议一个帐户及其分期付款。因此:

public class Account
{
    public Guid Id { get; set; }
    public string Description { get; set; } 
    public decimal TotalValue { get; set; }
    public List<Installment> Installments { get; set; } 
}

public class Installment
{
    public Guid Id { get; set; }
    public DateTime DueDate { get; set; }
    public decimal Value { get; set; }
    public Guid AccountId { get; set; }
}

非常常见的是,此配置中的对象被存储在其表中的关系数据库中,外国键用作参考,也就是关系。按照我使用的SQL的示例:

 CREATE TABLE Accounts
  (
     Id          UNIQUEIDENTIFIER PRIMARY KEY,
     Description NVARCHAR(255) NOT NULL,
     TotalValue  DECIMAL(18, 2) NOT NULL
  ); 

CREATE TABLE Installments
  (
     Id        UNIQUEIDENTIFIER PRIMARY KEY,
     DueDate   DATETIME NOT NULL,
     Value     DECIMAL(18, 2) NOT NULL,
     AccountId UNIQUEIDENTIFIER NOT NULL,
     FOREIGN KEY(AccountId) REFERENCES Accounts(Id)
  ); 

最常见的方法

我遇到的最常见的方法是恢复此类信息的最常见方法是对JOIN使用SQL咨询并输入映射,在我的情况下,这是您帐户的分期付款。

public const string ClassicQuery = @"
    SELECT acc.*, i.*  FROM Accounts acc
    inner join Installments i ON i.AccountId = acc.Id";

public List<Account> GetAllAccounts()
{
    var lookup = new Dictionary<Guid, Account>();

    _ = sqlConnection.Query<Account, Installment, Account>(SqlConstants.ClassicQuery,
        (acc, ins) =>
        {
            if (!lookup.TryGetValue(acc.Id, out var accEntry))
            {
                accEntry = acc;
                accEntry.Installments ??= new List<Installment>();
                lookup.Add(acc.Id, accEntry);
            }

            accEntry.Installments.Add(ins);
            return accEntry;

        }, splitOn: "Id");

    return lookup.Values.AsList();
}

breves的解释:1。对于已经习惯了SQL操作的人,您知道这种类型的咨询会生成表的笛卡尔产品,因此有必要在映射过程中帮助表“查找”。尝试这样做,而您最终将获得一个重复的帐户和关系。 2.我不确定,但是我相信C#的编译器能够意识到可以将硬质字符串优化为恒定或静态,但是我更喜欢明确地没有。因此,我们很快就会对其进行基准测试,我不想分配字符串来打扰结果。 3.母亲©All AsList()来自Dapper,并认为,如果已经实施了一个枚举作为List<T>,那么他将此列表重新为分配另一个的发明,他也会使用ToList()

另一种非常常见的方法是在.QueryMultiple()的结果上使用母亲时代.Read<T>()命令。因此:

public const string MultipleQuery = @"
    SELECT * FROM Accounts;
    SELECT * FROM Installments;";

public List<Account> GetAllAccounts()
{
    var res = sqlConnection.QueryMultiple(SqlConstants.MultipleQuery);

    var acc = res.Read<Account>();
    var installments = res.Read<Installment>();

    foreach (var account in acc)
    {
        account.Installments = installments.Where(i => i.AccountId == account.Id).ToList();
    }

    return acc.AsList();
}

道路非常简单和直接,没有母亲。当时,我从未质疑过这种情况的性能,可用性或可读性,实际上,在现实世界中,从来没有需要尝试改善这方面的任何事情。但是,当我“懒惰”时,我开始讨厌写这种咖啡厅的徒劳。

不是... Json?

对于那些已经冒险进入数据库的人。而且,如果我们冷冷地分析,我们可以尝试在关系银行中混合一点JSON,因为不,不是吗?包括SQL Server在内的所有主流发动机都已经制定了与表中的JSON打交道的指南,包括列表,插入,过滤器和数据库中的其他常见活动。在下面的过程中,我代表了许多以JSON形式的人的关系,并使用个性化的Dapper映射功能来读取和自动以这种格式记录帐户的分期付款。

public const string CreateAccountsJson = @"
    CREATE TABLE AccountsJson(
        Id UNIQUEIDENTIFIER PRIMARY KEY,
        Description NVARCHAR(255) NOT NULL,
        TotalValue DECIMAL(18,2) NOT NULL,
        Installments NVARCHAR(MAX) NULL
    );";


public class AccountJson
{
    public Guid Id { get; set; }
    public string Description { get; set; } 
    public decimal TotalValue { get; set; }
    public List<InstallmentJson> Installments { get; set; } 
}

public struct InstallmentJson
{
    public DateTime DueDate { get; set; }
    public decimal Value { get; set; }
}


public class InstallmentJsonTypeMapper 
    : SqlMapper.TypeHandler<List<InstallmentJson>>
{
    public override List<InstallmentJson> Parse(object value)
    {
        if (value is null) return new List<InstallmentJson>();

        var json = value.ToString();

        if (string.IsNullOrWhiteSpace(json)) return new List<InstallmentJson>();

        var res = JsonSerializer.Deserialize<List<InstallmentJson>>(json);

        if (res is null) return new List<InstallmentJson>();

        return res;
    }

    public override void SetValue(IDbDataParameter parameter, 
                                  List<InstallmentJson> value)
    {
        parameter.Value = value is null ? null : JsonSerializer.Serialize(value);
    }
}

工作,我知道它有效,但是会吗?您会在生产中使用它并感到安全吗?让我们使用BenchmarkDotNet来获得基准测试。我普及了一个本地数据库,其中有10个帐户,每个帐户分别有10个分期付款。结果是:

|     Method      | Accounts | Installments |   Mean    |  Gen0   |  Gen1  | Allocated  |
|-----------------|----------|--------------|----------:|--------:|-------:|-----------:|
|  ClassicQuery   |    10    |      1       | 153.5 us |  1.4648 |   -    |   9.38 KB  |
| MultipleQuery   |    10    |      1       | 135.7 us |  1.4648 |   -    |  10.16 KB  |
|    JsonQuery    |    10    |      1       | 146.0 us |  2.1973 |   -    |  14.51 KB  |
|  ClassicQuery   |    10    |      5       | 287.4 us |  3.9063 |   -    |  26.34 KB  |
| MultipleQuery   |    10    |      5       | 162.9 us |  2.9297 |   -    |  19.32 KB  |
|    JsonQuery    |    10    |      5       | 182.8 us |  5.1270 |   -    |  32.47 KB  |
|  ClassicQuery   |    10    |     10       | 376.6 us |  7.8125 |   -    |  48.88 KB  |
| MultipleQuery   |    10    |     10       | 204.1 us |  4.8828 |   -    |  31.19 KB  |
|    JsonQuery    |    10    |     10       | 214.3 us |  9.0332 | 0.2441 |  56.28 KB  |

老实说,我期望使用JSON的版本会更糟。内存的分配比其他记忆更高,但是执行时间已经装备了QueryMultiple()。对我来说,这已经成为一项好生意。
我们还可以停止考虑无知的方法。在分配和时间之间思考这是最糟糕的。

如果...数据结构怎么办?

您注意到恶作剧吗? Koud6测试是使用LINQ和一种非常简单的方法来映射帐户的帐户。我们可以使用与Cluessic方法中使用的相等的查找艺术家。请参阅:

public List<Account> GetAllAccounts()
    {
        var res = sqlConnection.QueryMultiple(SqlConstants.MultipleQuery);

        var accounts = res.Read<Account>(buffered: true).AsList();
        var installments = res.Read<Installment>();

        var lookup = new Dictionary<Guid, int>();

        for (int i = 0; i < accounts.Count; i++)
        {
            lookup.Add(accounts[i].Id, i);
        }

        foreach (var installment in installments)
        {
            if (lookup.TryGetValue(installment.AccountId, out int i))
            {
                accounts[i].Installments.Add(installment);
            }
        }

        return accounts;
    }

如果数据结构的名声是JUS,那么这是最好的,那么,让我们去测试。

|       Method       | Accounts | Installments |     Mean     |   Allocated  |
|:------------------:|:--------:|:------------:|------------:|------------:|
|   MultipleQuery    |     1    |      10      |    129.8 us |     6.18 KB  |
|     JsonQuery      |     1    |      10      |    121.9 us |     7.63 KB  |
|    MultipleLookup  |     1    |      10      |    121.8 us |     6.22 KB  |
|   MultipleQuery    |     1    |     100      |    188.2 us |     26.7 KB  |
|     JsonQuery      |     1    |     100      |    204.3 us |    48.57 KB  |
|    MultipleLookup  |     1    |     100      |    190.7 us |    26.74 KB  |
|   MultipleQuery    |    10    |      10      |    223.5 us |    31.19 KB  |
|     JsonQuery      |    10    |      10      |    236.4 us |    56.34 KB  |
|    MultipleLookup  |    10    |      10      |    197.2 us |    30.38 KB  |
|   MultipleQuery    |    10    |     100      |    720.0 us |   232.26 KB  |
|     JsonQuery      |    10    |     100      |  1,256.1 us |   465.83 KB  |
|    MultipleLookup  |    10    |     100      |    719.4 us |   231.43 KB  |
|   MultipleQuery    |   100    |      10      |  1,318.5 us |   276.86 KB  |
|     JsonQuery      |   100    |      10      |  1,244.9 us |   541.36 KB  |
|    MultipleLookup  |   100    |      10      |    751.1 us |   269.34 KB  |
|   MultipleQuery    |   100    |     100      | 13,786.4 us |  2386.42 KB  |
|     JsonQuery      |   100    |     100      | 12,036.7 us |  4635.98 KB  |
|    MultipleLookup  |   100    |     100      |  8,879.9 us |  2379.31 KB  |

为了帮助阅读结果,我们需要了解,尽管不超过100份,因为这不是一个非常现实的情况,但我们可以考虑,在其他情况下,对象可以在其层次结构中有一百个孩子。这就是使测试更有趣的原因。 通常,母亲©All MultipleLookup闪闪发光,这给我带来了一些悲伤,不得不接受我需要更多地担心SQL以确保表现愉快。
但是,就像每个好的“懒惰”一样,我并不容易放弃试图使最不可能成为可能。

E Se ...跨度?

很长一段时间以来,Span<T>已被用来以某种方式优化例程,而我在这里提出的建议是一种完全不常见的险恶方法,可以产生好水果。研究案件的相关对象由一个集合表示,因此成为Span<T>
标题的DAN是如何将一组分期付款转换为分期付款的Kou​​de10并保存到数据库中,并且答案的答案可能是在Bamme数据中,请参见:

public const string CreateAccountsSpan = @"
      CREATE TABLE AccountsSpan(
          Id UNIQUEIDENTIFIER PRIMARY KEY,
          Description NVARCHAR(255) NOT NULL,
          TotalValue DECIMAL(18,2) NOT NULL,
          Installments VARBINARY(MAX) NULL
      );";

public class InstallmentSpanTypeMapper 
    : SqlMapper.TypeHandler<List<InstallmentSpan>>
{
    public override List<InstallmentSpan> Parse(object value)
    {
        if (value is not byte[] bytes)
        {
            return new List<InstallmentSpan>();
        }

        var span = bytes.AsSpan();
        var structSpan = MemoryMarshal.Cast<byte, InstallmentSpan>(span);
        return  structSpan.ToArray().ToList();       
    }

    public override void SetValue(IDbDataParameter parameter,
                                  List<InstallmentSpan> value)
    {
        var s = CollectionsMarshal.AsSpan(value);
        Span<byte> span = MemoryMarshal.AsBytes(s);
        parameter.Value = span.ToArray();
    }
}

pout在陌生的母亲中,但它起作用。如果帐户中的分期付款是Koud12的Inves中的Installment[],则可以避免使用.ToArray().ToList()。必须有某种直接转换为列表的方法,但是我很懒惰。
让我们去测试吗?

|         Method | Accounts | Installments |        Mean |  Allocated |
|--------------- |--------- |------------- |------------:|-----------:|
|      JsonQuery |        1 |           10 |    127.2 us |    7.64 KB |
|      SpanQuery |        1 |           10 |    107.7 us |    3.55 KB |
| MultipleLookup |        1 |           10 |    124.9 us |    6.23 KB |
|      JsonQuery |        1 |          100 |    205.9 us |   48.57 KB |
|      SpanQuery |        1 |          100 |    113.9 us |      12 KB |
| MultipleLookup |        1 |          100 |    188.0 us |   26.73 KB |
|      JsonQuery |       10 |           10 |    211.9 us |    56.2 KB |
|      SpanQuery |       10 |           10 |    124.5 us |   15.34 KB |
| MultipleLookup |       10 |           10 |    197.9 us |   30.45 KB |
|      JsonQuery |       10 |          100 |  1,237.3 us |  465.74 KB |
|      SpanQuery |       10 |          100 |    192.0 us |   99.84 KB |
| MultipleLookup |       10 |          100 |    674.6 us |  231.42 KB |
|      JsonQuery |      100 |           10 |  1,207.0 us |  541.52 KB |
|      SpanQuery |      100 |           10 |    268.6 us |  132.97 KB |
| MultipleLookup |      100 |           10 |    747.2 us |  269.28 KB |
|      JsonQuery |      100 |          100 | 11,859.5 us | 4636.01 KB |
|      SpanQuery |      100 |          100 |  1,234.6 us |  976.76 KB |
| MultipleLookup |      100 |          100 |  9,424.9 us | 2379.24 KB |

现在是!它非常快速,并且分配的内存比其他内存要少得多,Porre有一个不利地位,如果某人需要通过SQL Server Management Studio等某些工具读取数据库,则不会成功看到那里的内容,并且如果您需要过滤器,也会有可能(我只是想)。

包括£o

我真的很喜欢Koud8版本,因为它简化了对数据库的咨询。另一方面,我不知道它在真实场景中真正有多少,具有许多其他属性甚至更嵌套的层次结构的真实对象。值得调查,因为它现在是另一扇门。使用JSON的版本是实用的,在SQL Server的最新版本中,已经可以直接在数据库中咨询和操纵JSON,我发现在需要存储非常复杂的对象并且始终使用非常复杂的对象的情况下,它会很大没有努力在团队堆栈中添加数据库。最后,我们可以反思以下事实:通常,我们没有写一张新的表,而不是每小时的新桌子,而不是新的重新定位,我的建议是与QueryMultiple保持联系,为每种情况应用一个结构良好的数据映射对象时,如果您确实需要优化某些内容,请尝试使用数据库BAMS跨度的此版本,并且在运行基准测试之前没有决定。

您想要道路吗?来aqui

ps(s):

  1. 测试环境使用本地数据库连接来减少网络时间,并且是通过这些配置的: BenchmarkDotnet = V0.13.5,OS = Windows 11(10.0.22621.1848/22H2/2022update/sunvalley2) Intel Core i7-8700 CPU 3.20GHz(咖啡湖),1个CPU,12逻辑和6个物理核心 .NET SDK = 7.0.300-preview.23122.5 [主机]:.NET 6.0.14(6.0.1423.7309),x64 ryujit avx2 DefaultJob:.net 6.0.14(6.0.1423.7309),x64 ryujit avx2 Microsoft SQL Server 2022(RTM -GDR)(KB5021522)-16.0.1050.5(x64)
  2. 我不是很懒惰,谁懒惰是文章中的角色。 Dapper性能案例经过了广泛的测试,我确实想向数据库中的跨度显示此Gambiarra。
  3. 这绝不能给我使用跨度的想法,我一个人考虑了:)