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不支持
相关推荐
刚子编程21 小时前
推荐一个开箱即用的.NET权限管理平台:Magic.NET
.net·开源项目·权限管理·企业级框架·后台脚手架
wwyyxx262 天前
Linux 下 .NET 程序 CPU 异常占用排查记录
linux·.net·调试
回忆2012初秋2 天前
.NET 时序数据操作实战:Apache IoTDB连接与 CRUD 完全指南
.net·apache·iotdb
回忆2012初秋2 天前
.NET 实战:Redis 缓存穿透、击穿与雪崩的原理剖析与解决方案
redis·缓存·.net
武藤一雄2 天前
19个核心算法(C#版)
数据结构·windows·算法·c#·排序算法·.net·.netcore
旡心-小小康3 天前
.NET WebSocket Socket
websocket·网络协议·.net
wenha3 天前
踩坑记录:UTF-8、UTF-8-BOM 与 GB2312 读取的乱码真相
utf-8·.net·编码·utf-8-bom
江沉晚呤时4 天前
C# 整型溢出处理机制:checked 与 unchecked 上下文解析
c#·.net
余衫马4 天前
在 Windows 服务中托管 ASP.NET Core Web API (.net6)
运维·windows·后端·asp.net·.net
步步为营DotNet4 天前
LM-Kit.NET:.NET 生态一站式本地 AI 开发平台
人工智能·.net