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
- 使用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,谢谢!!!
浙公网安备 33010602011771号