DBShadow横空出世,Dapper.net的天花板盖不住了

一、DBShadow是什么

  • DBShadow是.net开源的高性能ORM
  • DBShadow使用开源项目ShadowSql高效拼接sql
  • DBShadow使用开源项目PocoEmit.Mapper高效映射查询参数和查询结果
  • 也就是说SqlBuilder(ShadowSql)+OOM(PocoEmit.Mapper)=ORM(DBShadow)

二、DBShadow和Dapper对比一下

1. Dapper代码

复制代码
await using var conn = _dataSource.CreateConnection();
var sql = "SELECT \"Id\",\"Title\",\"Content\",\"Done\",\"LastTime\" FROM \"Todo\" WHERE \"Id\"=@Id";
var first = await conn.QueryFirstOrDefaultAsync<Todo>(sql, _todo);

DbDataSource _dataSource = new StringDataSource("Data Source=file::memory:;Cache=Shared", conn => new SqliteConnection(conn));

2. DBShadow代码

  • 使用SqliteEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
复制代码
var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);

ISqlEngine engine = new SqliteEngine();
ShadowExecutor _executor = ShadowBuilder.CreateCache(engine, Mapper.Default);
TodoTable _table = new("Todo");
ISelect _shadowSelect = _table.ToQuery()
    .And(_table.Id.Equal())
    .ToSelect()
    .SelectSelfColumns();

3. 用BenchmarkDotNet对比一下

  • DBShadow比Dapper快10%
  • 内存也占优
  • 以下是基于.net8,DBShadow支持.net10,Dapper没有.net10版本
  • 为了公平降级对比
  • 其实DBShadow在.net10下更快
Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Dapper 4.636 us 0.0194 us 0.0216 us 1.00 0.01 0.1400 - 2.38 KB 1.00
DBShadow 4.030 us 0.0152 us 0.0175 us 0.87 0.01 0.1300 - 2.2 KB 0.92

三、再用Mysql对比一下

1. Dapper代码

复制代码
await using var conn = _dataSource.CreateConnection();
var sql = "SELECT `Id`,`Title`,`Content`,`Done`,`LastTime` FROM `Todo` WHERE `Id`=@Id";
var first = await conn.QueryFirstOrDefaultAsync<Todo>(sql, _todo);

string ConnectionString = "Server=localhost;Database=Benchmarks;User=root;Password=123456;";
DbDataSource _dataSource = new MySqlDataSource(ConnectionString);

2. DBShadow代码

  • 使用MySqlEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
复制代码
var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);

ISqlEngine engine = new MySqlEngine();
ShadowCachedBuilder _executor = ShadowBuilder.CreateCache(engine, Mapper.Default);
TodoTable _table = new("Todo");
ISelect _shadowSelect = _table.ToQuery()
    .And(_table.Id.Equal())
    .ToSelect()
    .SelectSelfColumns();

3. 再用BenchmarkDotNet对比一下

  • DBShadow比Dapper只快3%
  • 内存占优
  • 由于MySql耗时几乎是Sqlite的100倍,执行代码是一样的(能快3%就很不容易了)
  • MySql慢也与我本机资源限制有关,使用docker搭建MySql,也没用固态硬盘(固态硬盘用在系统盘)
Method Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
Dapper 397.7 us 8.83 us 9.81 us 1.00 0.03 8.08 KB 1.00
DBShadow 383.9 us 14.62 us 15.64 us 0.97 0.04 7.23 KB 0.89

四、DBShadow支持事务操作

1. 举个事务回滚的栗子

  • 建表Accounts
  • 账号1初始化余额为100
  • 查询账号1余额为100
  • 开启事务
  • 使用事务把余额设置为90
  • 在事务下查询余额为90
  • 事务回滚
  • 再次查询账号1余额为100
  • DBShadow事务操作很优雅
  • 是否事务只与使用哪个处理器或数据源有关
  • 正常处理器或数据源可以很方便的转化为事务相关对象
复制代码
var table = new AccountTable();
try
{
    await SqliteExecutor.ExecuteAsync(table.ToCreate()); // 建表Accounts
}
catch { }
await new SingleInsert(table)
    .Insert(table.Id.InsertValue(1L))
    .Insert(table.Amount.InsertValue(100L))
    .ExecuteAsync(SqliteExecutor); // 账号1初始化余额为100
// 查询账号1
var query = table.ToSqlQuery().Where("Id=1");
var amount = await query
    .ToSelect()
    .Select(account => account.Amount)
    .GetScalarAsync<long>(SqliteExecutor); // 查询账号1余额为100
Assert.Equal(100L, amount);
// 开启事务
await using var transaction = await SqliteExecutor.BeginTransaction();
{
    await query.ToUpdate()
        .Set(account => account.Amount.AssignValue(90L))
        .ExecuteAsync(transaction); // 使用事务把余额设置为90
    var amount2 = await query
        .ToSelect()
        .Select(account => account.Amount)
        .GetScalarAsync<long>(transaction); // 在事务下查询余额为90
    // 减成了90
    Assert.Equal(90L, amount2);
    // 事务回滚
    await transaction.RollbackAsync();
}
var amount3 = await query
    .ToSelect()
    .Select(account => account.Amount)
    .GetScalarAsync<long>(SqliteExecutor);
// 回滚后恢复为100
Assert.Equal(100L, amount3);

2. 再举个事务提交和预编译的栗子

  • 事务提交和事务回滚特别相近,为此增加DBShadow预编译的内容
  • 建表预编译
  • 插入操作预编译
  • 查询账号余额预编译
  • 修改账号余额预编译
  • 建表Accounts
  • 账号1初始化余额为100
  • 查询账号1余额为100
  • 开启事务
  • 使用事务把余额设置为90
  • 在事务下查询余额为90
  • 事务提交
  • 再次查询账号1余额为90
  • 预编译能提高执行性能和稳定性
  • 在事务操作之前预编译很有必要
  • 预编译之后的结果对是否事务数据源都是一样的使用方式(也就是业务代码可以做到通用)
复制代码
var builder = SqliteExecutor.Builder;
var table = new AccountTable();        
var query = table.ToSqlQuery().Where("Id=1");

#region Compile
// 建表预编译
var createCompiled = builder.BuildQuery(table.ToCreate());
// 插入操作预编译
var insertCompiled = builder.BuildQuery(new SingleInsert(table)
    .Insert(table.Id.InsertValue(1L))
    .Insert(table.Amount.InsertValue(100L)));
// 查询账号余额预编译
var amountCompiled = builder.BuildScalar(query
    .ToSelect()
    .Select(account => account.Amount));
// 修改账号余额预编译
var updateCompiled = builder.BuildQuery(query.ToUpdate()
    .Set(account => account.Amount.AssignValue(90L)));
#endregion

try
{
    await createCompiled.ExecuteAsync(SqliteSource); // 建表Accounts
}
catch { }
await insertCompiled.ExecuteAsync(SqliteSource); // 账号1初始化余额为100
var amount = await amountCompiled.GetScalarAsync<long>(SqliteSource); // 查询账号1余额为100
Assert.Equal(100L, amount);
// 开启事务
await using var transaction = await SqliteSource.BeginTransaction();
{
    await updateCompiled.ExecuteAsync(transaction); // 使用事务把余额设置为90
    var amount2 = await amountCompiled.GetScalarAsync<long>(transaction); // 在事务下查询余额为90
    Assert.Equal(90L, amount2);
    await transaction.CommitAsync(); // 事务提交
}
var amount3 = await amountCompiled.GetScalarAsync<long>(SqliteSource); // 再次查询账号1余额为90
Assert.Equal(90L, amount3);

五、DBShadow解密

1. 首先DBShadow基于现代ADO.net

1.1 DbDataSource
  • 数据连接基于System.Data.Common.DbDataSource
  • DbDataSource的重要方法CreateConnection
  • 相当于数据库连接工厂或连接池
1.2 StringDataSource
  • 虽然微软推出DbDataSource很多年了,但是业界支持的并不是很好
  • 比如Sqlite不支持DbDataSource
  • 就算是System.Data也只能.net7+才支持
  • 这个破破烂烂的世界,需要缝缝补补
  • StringDataSource支持net4.5+和netstandard2.0+
  • 在.net7+下StringDataSource是DbDataSource的子类
  • 其他情况下DBShadow使用StringDataSource直接代替DbDataSource
1.3 IAsyncEnumerable<>
  • 这是异步下的迭代器
  • 在异步操作IO流下实现延迟加载和流式计算
  • DBShadow的列表都是基于IAsyncEnumerable<>
  • EFCore也支持IAsyncEnumerable<>,但Dapper不支持
相关推荐
微小冷2 小时前
海康威视相机C#二次开发环境配置
数码相机·c#·.net·海康威视·相机开发
唐青枫6 小时前
别只会解析参数:C#.NET Spectre.Console.Cli 命令行工具实战详解
c#·.net
rockey6271 天前
基于AScript的Lua脚本语言发布啦
c#·.net·lua·script·动态脚本
半亩码田1 天前
【.NET新特性·第11篇】C# 14 字段属性:告别手写 backing field
java·c#·.net
唐青枫1 天前
别再把命令行参数写成一团:C#.NET System.CommandLine 实战详解
c#·.net
hez20101 天前
让 .NET 11 的泛型虚方法变得更快!
c#·.net·.net core·clr·compiler
宝桥南山1 天前
Microsoft Agent Framework(.NET) - 尝试一下从MCP Server上获取Agent Skills
ai·微软·c#·aigc·.net·.netcore
海盗12342 天前
微软技术日报·2026-08-12——Windows 11多通道累积更新推送十余项功能改进;.NET WebSocket DoS紧急修复
windows·microsoft·.net
唐青枫2 天前
别再手写 args[0]:C#.NET CommandLineParser 命令行工具实战详解
c#·.net
.Peter2 天前
.NET/WPF 程序在部分 Windows 电脑无法保存用户环境变量:使用 DPAPI 实现安全兼容
windows·信息安全·c#·.net·wpf·环境变量·dpapi