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不支持
相关推荐
CSharp精选营20 小时前
.NET 8 与 .NET 9 支持终止倒计时:开发者需要了解什么
.net·lts·.net8·.net9·码农刚子·升级迁移·sts·支持终止
hez20103 天前
在 .NET 上构建超大托管数组
c#·.net·.net core·gc·clr
唐青枫10 天前
线程不是越多越快:C#.NET Thread 生命周期、同步与后台工作线程实战
c#·.net
唐青枫11 天前
别只会反射:C#.NET Emit 动态生成代码实战详解
c#·.net
Caco_D11 天前
一行代码抓遍全网 20 个热榜!Aneiang.Pa 4.0 发布 — 极简 .NET 爬虫库
爬虫·.net
咕白m62511 天前
.NET 环境下 Word 超链接批量提取方案
c#·.net
小码编匠12 天前
C# 工控上位机必备:数据转换工具类与十个核心模块
后端·c#·.net
唐青枫14 天前
别再乱用 StartNew:C#.NET TaskFactory 任务调度实战详解
c#·.net