一、mysql参数的成本
- 使用BenchmarkDotNet测试
1. 测试代码如下
- CreateParameter直接构造参数
- Clone预先构造参数名和类型,复制后只设置参数值
/* by 01130.hk - online tools website : 01130.hk/zh/html2all.html */ private static readonly MySqlCommand _command = new(); private static MySqlParameter _idParameter; [Benchmark(Baseline = true)] public DbParameter CreateParameter() { var id = _command.CreateParameter(); id.ParameterName = "Id"; id.DbType = System.Data.DbType.Int64; id.Value = 1L; return id; } [Benchmark] public DbParameter Clone() { var id = _idParameter.Clone(); id.Value = 1L; return id; } [GlobalSetup] public void Setup() { _idParameter = _command.CreateParameter(); _idParameter.ParameterName = "Id"; _idParameter.DbType = System.Data.DbType.Int64; }2. 测试结果如下
- 通过复制方式节省了80%的时间
- 感觉有搞头,所以希望把复制参数的功能加入到DBShadow.net中用来提高性能
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|
| CreateParameter | 58.60 ns | 0.185 ns | 0.213 ns | 1.00 | 0.0064 | 112 B | 1.00 |
| Clone | 11.01 ns | 0.387 ns | 0.431 ns | 0.19 | 0.0064 | 112 B | 1.00 |
二、Clone重写参数预编译
1. 生成的代码如下
- 预编译反射command和参数类型,以便生成更原生更快的代码
- 其中cached是预先构造好的参数缓存作为常量
- cached含有参数名和类型信息
- Clone后只需要设置参数值即可
/* by 01130.hk - online tools website : 01130.hk/zh/html2all.html */ (DbCommand command, Todo param) => { MySqlParameterCollection parameters = ((MySqlCommand)command).Parameters; MySqlParameter t = cached.Clone(); t.Value = param.Id; // MySqlParameter Add(MySqlParameter parameter) return parameters.Add(t); }2. 选择更合适的原生方法
- MySqlParameterCollection有两个Add方法
- 很明显重载Add(MySqlParameter parameter)更合适
- 调用重载Add(object value)需要做多次类型转换
- 既然生成的代码可以控制,就需要选用更合适的重载
public MySqlParameter Add(MySqlParameter parameter); public override int Add(object value);3. 类型嗅探
- 预编译是ShadowBuilder负责,基于ADO.net(DbCommand)
- 为了生成更原生的代码,需要知道具体的Command类型
- 所以ShadowBuilder需要更多实际的类型信息
- 为此本来ShadowExecutor才需要的数据源信息,现在ShadowBuilder也需要
3.1 ShadowBuilder以前的代码
class ShadowBuilder(ISqlEngine engine, IMapperOptions options);3.2 ShadowBuilder现在的代码
- 其中CommandBuilder可以通过DbDataSourcet推导出来
- 也就是实际只增加了DbDataSource
class ShadowBuilder(IMapperOptions options, ISqlEngine engine, DbDataSource dataSource, CommandBuilder commandBuilder);var command = dataSource.CreateConnection().CreateCommand(); var parameterType = command.CreateParameter().GetType(); CommandBuilder commandBuilder = CommandBuilder.Create(command, parameterType);3.3 嗅探的过程
- 通过DbDataSource推导出CommandBuilder
- 其中CreateConnection、CreateCommand和CreateParameter等方法并不实际执行数据库IO操作,只是用来嗅探类型信息
var command = dataSource.CreateConnection().CreateCommand(); var commandType = command.GetType(); var parametersProperty = commandType.GetProperty("Parameters", BindingFlags.Instance | BindingFlags.DeclaredOnly); var parametersType = parametersProperty.PropertyType; var parameterType = command.CreateParameter().GetType(); var addParameterMethod = parametersType.GetMethod("Add", [parameterType]);4. ShadowExecutor变化比较小
- 只是把类ShadowBuilder改为IShadowBuilder接口
- 实际还是ShadowBuilder变化
4.1 ShadowExecutor以前的代码
class ShadowExecutor(ShadowBuilder builder, SqlSource source);4.2 ShadowExecutor现在的代码
class ShadowExecutor(IShadowBuilder builder, SqlSource source);5. ShadowBuilder和ShadowCachedBuilder的关系
5.1 ShadowCachedBuilder以前是ShadowBuilder的子类
class ShadowCachedBuilder : ShadowBuilder;5.2 ShadowCachedBuilder现在是ShadowBuilder的包装类
- 通过接口IShadowBuilder来实现
- 通过original成员调用原有的ShadowBuilder功能
- 这样避免ShadowBuilder的复杂度增加影响到ShadowCachedBuilder
- ShadowCachedBuilder只负责缓存编译好的对象
class ShadowCachedBuilder(ShadowBuilder original) : IShadowBuilder;三、复杂的现实世界
1. SqliteParameter不支持复制
- SqliteParameter没有实现ICloneable接口
- SqliteParameter也没有Clone方法
2. SqlParameter可以复制,但性能不佳
- SqlParameter(Mssql)实现了ICloneable接口
2.1 测试代码如下
private readonly SqlCommand _command = new(); private ICloneable _idParameter; [Benchmark(Baseline = true)] public DbParameter CreateParameter() { var id = _command.CreateParameter(); id.ParameterName = "Id"; id.DbType = System.Data.DbType.Int64; id.Value = 1L; return id; } [Benchmark] public DbParameter Clone() { var id = (SqlParameter)_idParameter.Clone(); id.Value = 1L; return id; } [GlobalSetup] public void Setup() { var idParameter = _command.CreateParameter(); idParameter.ParameterName = "Id"; idParameter.DbType = System.Data.DbType.Int64; _idParameter = idParameter; }2.2 测试结果如下
- Clone方式比直接CreateParameter方式还慢
- 这就是为什么CommandBuilder可以推导出来还要作为ShadowBuilder参数的原因
- 现在只能把选择权交给用户,让用户决定是否启用Clone方式
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| CreateParameter | 15.38 ns | 0.942 ns | 0.967 ns | 14.53 ns | 1.00 | 0.09 | 0.0106 | 184 B | 1.00 |
| Clone | 24.84 ns | 0.169 ns | 0.194 ns | 24.81 ns | 1.62 | 0.10 | 0.0106 | 184 B | 1.00 |
四、ParameterBuilder
- ParameterBuilder负责反射参数实际类型和方法,用于生成更高效的代码
- ParameterBuilder是抽象类,有3个具体实现
1. BuildByNamedConstructor
- 通过含参数名构造函数创建参数
2. BuildByDefaultConstructor
- 通过默认构造函数创建参数,然后设置参数名
3. BuildByMethod
- 通过command的CreateParameter方法创建参数,然后设置参数名
4. ParameterBuilder的成员
- New方法用于创建参数实例
- SetParameterName方法用于设置参数的ParameterName属性
- SetDbType方法用于设置参数的DbType属性
- SetValue方法用于设置参数的Value属性
- GetParameterName方法用于生成集合参数的子参数名
5. 集合参数
- 大部分数据库并不支持集合参数
- 因此需要生成多个单独的子参数来实际执行
- eg: IN @Ids 需要转化为 IN (@Ids0, @Ids1, @Ids2),Ids为集合参数
- 集合参数很特殊,需要实际执行的时候才确定实际子参数的个数
- 通过预编译可以生成遍历实参集合生成子参数的代码
五、IParameterFactory
- IParameterFactory就是参数处理的抽象
- ParameterFactory是默认实现
- CloneParameterFactory是Clone方式实现
- 而IParameterFactory作为CommandBuilder的成员,处理参数部分的逻辑
1. ParameterFactory
- 默认的参数处理实现
- ParameterFactory包含ParameterBuilder,用于生成更高效的参数处理代码
- CloneParameterFactory也需要调用ParameterFactory的功能
1.1 数据库类型处理
- 可以通过重写CheckDbTypeCore方法来处理特殊的数据库类型映射需求
/// <summary> /// 处理数据库类型(预留扩展处理特殊需求) /// </summary> /// <param name="valueType"></param> protected virtual DbType CheckDbTypeCore(Type valueType) => CheckDbType(valueType); /// <summary> /// 默认数据库类型 /// </summary> /// <param name="valueType"></param> /// <returns></returns> public static DbType CheckDbType(Type valueType) { if (valueType.IsArray) return DbType.Binary; var typeCode = Type.GetTypeCode(valueType); return typeCode switch { TypeCode.Byte => DbType.Byte, TypeCode.SByte => DbType.SByte, TypeCode.Int16 => DbType.Int16, TypeCode.UInt16 => DbType.UInt16, TypeCode.Int32 => DbType.Int32, TypeCode.UInt32 => DbType.UInt32, TypeCode.Int64 => DbType.Int64, TypeCode.UInt64 => DbType.UInt64, TypeCode.Single => DbType.Single, TypeCode.Double => DbType.Double, TypeCode.Decimal => DbType.Decimal, TypeCode.Boolean => DbType.Boolean, TypeCode.String => DbType.String, TypeCode.Char => DbType.StringFixedLength, TypeCode.DateTime => DbType.DateTime, _ => DbType.Object, }; }1.2 CreateParameter方法
- 实际CreateParameter专用于CloneParameterFactory
- 用于创建参数原型,以便后续Clone使用
/// <summary> /// 构造参数 /// </summary> /// <param name="valueType"></param> /// <returns></returns> DbParameter CreateParameter(Type valueType); /// <summary> /// 构造参数 /// </summary> /// <param name="name"></param> /// <param name="valueType"></param> /// <returns></returns> DbParameter CreateParameter(string name, Type valueType);1.3 实现接口IParameterBuilder
- 以下Create方法实际用于构造参数表达式
- 其中_parameterBuilder就是ParameterBuilder实例,前面有介绍
public Expression Create(IEmitBuilder builder, string name, Expression value) => Create(builder, Expression.Constant(name), value); /// <summary> /// 构造参数 /// </summary> /// <param name="builder"></param> /// <param name="parameterName"></param> /// <param name="value"></param> /// <returns></returns> public Expression Create(IEmitBuilder builder, Expression parameterName, Expression value) { // var parameter = command.CreateParameter(); // parameter.ParameterName = parameterName; var parameter = _parameterBuilder.New(builder, parameterName); // parameter.DbType = dbType; _parameterBuilder.SetDbType(builder, parameter, CheckDbTypeCore(value.Type)); // parameter.Value = value; _parameterBuilder.SetValue(builder, parameter, value); return parameter; }1.4 实现接口ICollectParameterBuilder
- ICollectParameterBuilder用于处理集合参数的子参数
public Expression CreateIndex(IEmitBuilder builder, Expression prefix, Expression index, Expression value) => Create(builder, ParameterBuilder.GetParameterName(prefix, index), value);2. CloneParameterFactory
- CloneParameterFactory由CloneParameterBuilder和CloneCollectParameterBuilder组成
- CloneParameterBuilder通过Clone方式创建参数
- CloneCollectParameterBuilder通过Clone方式创建集合参数
3. CloneParameterBuilder
3.1 CloneParameterBuilder包含ParameterBuilder、ParameterFactory和IEmitConverter成员
- ParameterBuilder用于处理参数值
- ParameterFactory用于生成参数原型,以便Clone使用
- IEmitConverter用于复制参数
class CloneParameterBuilder(ParameterBuilder original, ParameterFactory factory, IEmitConverter cloneConverter);3.2 GetProtoType方法
- 用于生成参数原型并缓存
- 调用ParameterFactory的CreateParameter方法生成参数原型
- 按参数名和类型缓存
/// <summary> /// 获取原型缓存 /// </summary> /// <param name="name"></param> /// <param name="valueType"></param> /// <returns></returns> public DbParameter GetProtoType(string name, Type valueType) { var key = new NameTypedCacheKey(name, valueType); if (_protoTypes.TryGetValue(key, out var cached)) return cached; #if NET9_0_OR_GREATER lock (_lock) #else lock (_protoTypes) #endif { if (_protoTypes.TryGetValue(key, out cached)) return cached; return _protoTypes[key] = _factory.CreateParameter(name, valueType); } }3.3 Create方法
- Create用于构造参数表达式
- 先调用GetProtoType方法获取参数原型
- 该过程发生在预编译阶段,不会影响运行时性能
- 通过缓存避免不同方法相同参数原型的重复创建
- 通过cloneConverter生成Clone调用表达式
- 最后调用ParameterBuilder设置参数值
4. CloneCollectParameterBuilder
- CloneCollectParameterBuilder用于处理集合参数
3.1 CloneCollectParameterBuilder也是包含ParameterBuilder、ParameterFactory和IEmitConverter成员
- ParameterBuilder用于处理参数值
- ParameterFactory用于生成参数原型,以便Clone使用
- IEmitConverter用于复制参数
class CloneCollectParameterBuilder(ParameterBuilder original, ParameterFactory factory, IEmitConverter cloneConverter);3.2 GetProtoType方法
- 用于生成集合参数原型并缓存
- 调用ParameterFactory的CreateParameter方法生成参数原型
- 按类型缓存
- CollectParameterPrototype(集合参数原型)用于实际处理集合参数的子参数
/// <summary> /// 获取原型缓存 /// </summary> /// <param name="valueType"></param> /// <returns></returns> public CollectParameterPrototype GetProtoType(Type valueType) { if (_protoTypes.TryGetValue(valueType, out var protoType)) return protoType; #if NET9_0_OR_GREATER lock (_lock) #else lock (_protoTypes) #endif { if (_protoTypes.TryGetValue(valueType, out protoType)) return protoType; var parameter = _factory.CreateParameter(valueType); return _protoTypes[valueType] = new(_original, parameter, _cloneConverter); } }4. CollectParameterPrototype
- 集合参数原型
4.1 包含ParameterBuilder、IEmitConverter和原型缓存
class CollectParameterPrototype(ParameterBuilder original, ConstantExpression cached, IEmitConverter cloneConverter);4.2 CreateIndex方法
- CreateIndex用于构造集合参数的子参数
- 通过cloneConverter生成Clone调用表达式
- 最后调用ParameterBuilder设置参数名和参数值
六、ToExecutor
- 由于ShadowBuilder包含数据源可以很方便转化为ShadowExecutor
- 增加ToExecutor方法简化操作
- ToExecutor是IShadowBuilder的方法,所以ShadowCachedBuilder也支持
/// <summary> /// 转化为执行器 /// </summary> /// <param name="commandTimeout"></param> /// <returns></returns> ShadowExecutor ToExecutor(int? commandTimeout = null);七、总结
- 通过嗅探实际类型生成更高效的参数处理代码
- 不同数据库差异为此带来了复杂性
- 部分数据库可以通过Clone方式创建参数提高性能
1. 一般使用示例
- 一般使用CreateCache方法创建ShadowBuilder
- 如果是一次性使用无需缓存可以使用Create方法代替
var engine = new MySqlEngine(); var dataSource = new MySqlDataSource(ConnectionString); var builder = ShadowBuilder.CreateCache(Mapper.Default, engine, dataSource); var select = table.ToQuery() .And(table.Id.Equal()) .ToSelect() .SelectSelfColumns(); var compiled = select.BuildQuery<Todo, Todo>(builder);1.1 参数处理生成如下代码
(DbCommand command, Todo param) => { var parameters = ((MySqlCommand)command).Parameters; var t = new MySqlParameter(); t.ParameterName = "Id"; t.DbType = DbType.Int64; t.Value = param.Id; return parameters.Add(t); }2. 启用Clone方式示例
- 启用Clone要复杂一点
- 需要通过CloneParameter生成一个ParameterFactory
- 然后传入ShadowBuilder的CreateCache方法中
var engine = new MySqlEngine(); var dataSource = new MySqlDataSource(ConnectionString); var parameterFactory = ParameterFactory.CloneParameter(new MySqlCommand()) var builder = ShadowBuilder.CreateCache(Mapper.Default, engine, dataSource, parameterFactory); var select = table.ToQuery() .And(table.Id.Equal()) .ToSelect() .SelectSelfColumns(); var compiled = select.BuildQuery<Todo, Todo>(builder);2.1 参数处理生成如下代码
- cached是预先构造好作为常量的参数缓存
- 很明显如果Clone比new更变快的话,这个代码会更高效
- Mysql下此方法耗时为原来的20%(也就是性能提高4倍)
(DbCommand command, Todo param) => { var parameters = ((MySqlCommand)command).Parameters; var t = cached.Clone(); t.Value = param.Id; return parameters.Add(t); }另外源码托管地址: https://github.com/donetsoftwork/DBShadow.net ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/DBShadow.net
如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!