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

一、DBShadow是什么

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

二、DBShadow和Dapper对比一下

1. Dapper代码

csharp 复制代码
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);
csharp 复制代码
DbDataSource _dataSource = new StringDataSource("Data Source=file::memory:;Cache=Shared", conn => new SqliteConnection(conn));

2. DBShadow代码

  • 使用SqliteEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
csharp 复制代码
var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);
csharp 复制代码
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%
  • 内存也占优
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代码

csharp 复制代码
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);
csharp 复制代码
string ConnectionString = "Server=localhost;Database=Benchmarks;User=root;Password=123456;";
DbDataSource _dataSource = MySqlDataSource(ConnectionString);

2. DBShadow代码

  • 使用MySqlEngine处理数据库方言
  • 使用Mapper.Default处理类型映射
  • ShadowCachedBuilder用来编译和缓存
csharp 复制代码
var first = await _shadowSelect.GetFirstAsync<Todo, Todo?>(_executor, _todo);
csharp 复制代码
ISqlEngine engine = new MySqlEngine();
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只快3%
  • 内存占优
  • 由于MySql耗时几乎是Sqlite的100倍,执行代码是一样的(能快3%就很不容易了)
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事务操作很优雅
  • 是否事务只与使用哪个处理器或数据源有关
  • 正常处理器或数据源可以很方便的转化为事务相关对象
csharp 复制代码
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
  • 预编译能提高执行性能也稳定性
  • 再事务操作之前预编译很有必要
  • 预编译之后的结果对是否事务数据源都是一样的使用方式(也就是业务代码可以做到通用)
csharp 复制代码
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

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

gitee同步更新:https://gitee.com/donetsoftwork/DBShadow.net

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