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_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_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. ShadowSql

3. PocoEmit.Mapper

4. DBShadow为啥能比Dapper快

  • Dapper通过正则表达式提取参数
  • 而DBShadow从ShadowSql的语义中获取参数
  • 对于ShadowSql参数本身就是个独立语义对象,很容易提取
  • 从ShadowSql中也可以提取到字段名可以通过预编译进行加速
  • DBShadow必须明确是否有参数,形参类型明确,这样才好提前优化
  • DBShadow不支持动态类型作为参数和返回类型,也是为了提前编译优化
  • DBShadow基于ShadowSql明确的语义
  • DBShadow支持预编译
  • 而Dapper基于sql的通用语法及各种方言,只能各种兼容,拖累了性能
  • 当然也有ShadowSql和PocoEmit的高性能加持

另外源码托管地址: https://github.com/donetsoftwork/DBShadow.net ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/DBShadow.net

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

posted on 2026-01-15 09:44  xiangji  阅读(171)  评论(2)    收藏  举报

导航