找回密码
 立即注册
首页 业界区 业界 DBShadow横空出世,Dapper.net的天花板盖不住了 ...

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

百杲憔 2026-1-15 14:10:03
一、DBShadow是什么



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

1. Dapper代码
  1. await using var conn = _dataSource.CreateConnection();
  2. var sql = "SELECT "Id","Title","Content","Done","LastTime" FROM "Todo" WHERE "Id"=@Id";
  3. var first = await conn.QueryFirstOrDefaultAsync<Todo>(sql, _todo);
复制代码
  1. DbDataSource _dataSource = new StringDataSource("Data Source=file::memory:;Cache=Shared", conn => new SqliteConnection(conn));
复制代码
2. DBShadow代码



  • 使用SqliteEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
  1. var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);
复制代码
  1. ISqlEngine engine = new SqliteEngine();
  2. ShadowExecutor _executor = ShadowBuilder.CreateCache(engine, Mapper.Default);
  3. TodoTable _table = new("Todo");
  4. ISelect _shadowSelect_shadowSelect = _table.ToQuery()
  5.     .And(_table.Id.Equal())
  6.     .ToSelect()
  7.     .SelectSelfColumns();
复制代码
3. 用BenchmarkDotNet对比一下



  • DBShadow比Dapper快10%
  • 内存也占优
  • 以下是基于.net8,DBShadow支持.net10,Dapper没有.net10版本
  • 为了公平降级对比
  • 其实DBShadow在.net10下更快
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc RatioDapper4.636 us0.0194 us0.0216 us1.000.010.1400-2.38 KB1.00DBShadow4.030 us0.0152 us0.0175 us0.870.010.1300-2.2 KB0.92三、再用Mysql对比一下

1. Dapper代码
  1. await using var conn = _dataSource.CreateConnection();
  2. var sql = "SELECT `Id`,`Title`,`Content`,`Done`,`LastTime` FROM `Todo` WHERE `Id`=@Id";
  3. var first = await conn.QueryFirstOrDefaultAsync<Todo>(sql, _todo);
复制代码
  1. string ConnectionString = "Server=localhost;Database=Benchmarks;User=root;Password=123456;";
  2. DbDataSource _dataSource = new MySqlDataSource(ConnectionString);
复制代码
2. DBShadow代码



  • 使用MySqlEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
  1. var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);
复制代码
  1. ISqlEngine engine = new MySqlEngine();
  2. ShadowCachedBuilder _executor = ShadowBuilder.CreateCache(engine, Mapper.Default);
  3. TodoTable _table = new("Todo");
  4. ISelect _shadowSelect_shadowSelect = _table.ToQuery()
  5.     .And(_table.Id.Equal())
  6.     .ToSelect()
  7.     .SelectSelfColumns();
复制代码
3. 再用BenchmarkDotNet对比一下



  • DBShadow比Dapper只快3%
  • 内存占优
  • 由于MySql耗时几乎是Sqlite的100倍,执行代码是一样的(能快3%就很不容易了)
  • MySql慢也与我本机资源限制有关,使用docker搭建MySql,也没用固态硬盘(固态硬盘用在系统盘)
MethodMeanErrorStdDevRatioRatioSDAllocatedAlloc RatioDapper397.7 us8.83 us9.81 us1.000.038.08 KB1.00DBShadow383.9 us14.62 us15.64 us0.970.047.23 KB0.89四、DBShadow支持事务操作

1. 举个事务回滚的栗子



  • 建表Accounts
  • 账号1初始化余额为100
  • 查询账号1余额为100
  • 开启事务
  • 使用事务把余额设置为90
  • 在事务下查询余额为90
  • 事务回滚
  • 再次查询账号1余额为100
  • DBShadow事务操作很优雅
  • 是否事务只与使用哪个处理器或数据源有关
  • 正常处理器或数据源可以很方便的转化为事务相关对象
  1. var table = new AccountTable();
  2. try
  3. {
  4.     await SqliteExecutor.ExecuteAsync(table.ToCreate()); // 建表Accounts
  5. }
  6. catch { }
  7. await new SingleInsert(table)
  8.     .Insert(table.Id.InsertValue(1L))
  9.     .Insert(table.Amount.InsertValue(100L))
  10.     .ExecuteAsync(SqliteExecutor); // 账号1初始化余额为100
  11. // 查询账号1
  12. var query = table.ToSqlQuery().Where("Id=1");
  13. var amount = await query
  14.     .ToSelect()
  15.     .Select(account => account.Amount)
  16.     .GetScalarAsync<long>(SqliteExecutor); // 查询账号1余额为100
  17. Assert.Equal(100L, amount);
  18. // 开启事务
  19. await using var transaction = await SqliteExecutor.BeginTransaction();
  20. {
  21.     await query.ToUpdate()
  22.         .Set(account => account.Amount.AssignValue(90L))
  23.         .ExecuteAsync(transaction); // 使用事务把余额设置为90
  24.     var amount2 = await query
  25.         .ToSelect()
  26.         .Select(account => account.Amount)
  27.         .GetScalarAsync<long>(transaction); // 在事务下查询余额为90
  28.     // 减成了90
  29.     Assert.Equal(90L, amount2);
  30.     // 事务回滚
  31.     await transaction.RollbackAsync();
  32. }
  33. var amount3 = await query
  34.     .ToSelect()
  35.     .Select(account => account.Amount)
  36.     .GetScalarAsync<long>(SqliteExecutor);
  37. // 回滚后恢复为100
  38. Assert.Equal(100L, amount3);
复制代码
2. 再举个事务提交和预编译的栗子



  • 事务提交和事务回滚特别相近,为此增加DBShadow预编译的内容
  • 建表预编译
  • 插入操作预编译
  • 查询账号余额预编译
  • 修改账号余额预编译
  • 建表Accounts
  • 账号1初始化余额为100
  • 查询账号1余额为100
  • 开启事务
  • 使用事务把余额设置为90
  • 在事务下查询余额为90
  • 事务提交
  • 再次查询账号1余额为90
  • 预编译能提高执行性能和稳定性
  • 在事务操作之前预编译很有必要
  • 预编译之后的结果对是否事务数据源都是一样的使用方式(也就是业务代码可以做到通用)
  1. var builder = SqliteExecutor.Builder;
  2. var table = new AccountTable();        
  3. var query = table.ToSqlQuery().Where("Id=1");
  4. #region Compile
  5. // 建表预编译
  6. var createCompiled = builder.BuildQuery(table.ToCreate());
  7. // 插入操作预编译
  8. var insertCompiled = builder.BuildQuery(new SingleInsert(table)
  9.     .Insert(table.Id.InsertValue(1L))
  10.     .Insert(table.Amount.InsertValue(100L)));
  11. // 查询账号余额预编译
  12. var amountCompiled = builder.BuildScalar(query
  13.     .ToSelect()
  14.     .Select(account => account.Amount));
  15. // 修改账号余额预编译
  16. var updateCompiled = builder.BuildQuery(query.ToUpdate()
  17.     .Set(account => account.Amount.AssignValue(90L)));
  18. #endregion
  19. try
  20. {
  21.     await createCompiled.ExecuteAsync(SqliteSource); // 建表Accounts
  22. }
  23. catch { }
  24. await insertCompiled.ExecuteAsync(SqliteSource); // 账号1初始化余额为100
  25. var amount = await amountCompiled.GetScalarAsync<long>(SqliteSource); // 查询账号1余额为100
  26. Assert.Equal(100L, amount);
  27. // 开启事务
  28. await using var transaction = await SqliteSource.BeginTransaction();
  29. {
  30.     await updateCompiled.ExecuteAsync(transaction); // 使用事务把余额设置为90
  31.     var amount2 = await amountCompiled.GetScalarAsync<long>(transaction); // 在事务下查询余额为90
  32.     Assert.Equal(90L, amount2);
  33.     await transaction.CommitAsync(); // 事务提交
  34. }
  35. var amount3 = await amountCompiled.GetScalarAsync<long>(SqliteSource); // 再次查询账号1余额为90
  36. 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



  • 使用ShadowSql拼接sql
  • ShadowSql也是笔者的开源项目,之前就有很多相关的介绍
  • ShadowSql.net之正确使用方式
3. PocoEmit.Mapper



  • 使用PocoEmit.Mapper来映射
  • 把参数映射为SqlParameter
  • 把DataReader映射为实体
  • 使得DBShadow继承了PocoEmit.Mapper的可配置性、可扩展性和依赖注入等功能,这些EFCore和Dapper都是做不到的
  • PocoEmit.Mapper也是笔者的开源项目,之前就有很多相关的介绍
  • 婶可忍叔不可忍的AutoMapper,你还用吗?
  • 如何使用PocoEmit.Mapper替代AutoMapper
  • PocoEmit遥遥领先于AutoMapper之打通充血模型的任督二脉
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,谢谢!!!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

2026-1-16 05:15:06

举报

2026-1-16 20:11:42

举报

懂技术并乐意极积无私分享的人越来越少。珍惜
2026-1-22 21:31:25

举报

2026-1-25 11:43:17

举报

2026-1-26 11:04:08

举报

2026-2-6 23:41:57

举报

12下一页
您需要登录后才可以回帖 登录 | 立即注册