diff --git "a/doc/\347\211\210\346\234\254\346\227\245\345\277\227.md" "b/doc/\347\211\210\346\234\254\346\227\245\345\277\227.md" index 1471a93a..da1dc538 100644 --- "a/doc/\347\211\210\346\234\254\346\227\245\345\277\227.md" +++ "b/doc/\347\211\210\346\234\254\346\227\245\345\277\227.md" @@ -1,5 +1,10 @@ # 版本功能更新记录 +## v2026.08.26 +- 🐞修复`ExecuteResult`中的DataReader重复Close +- 🐞修复实体映射处理匿名类型中的class类型 +- 🛠重构`SqlExecutor`,将连接和事务管理提升到ExpressionContext中 + ## v2026.08.21 - 🛠重构Table生成配置,基类`TableOptions`保留公共配置,由各Provider派生各自的配置项 - ⚡️新增`StringBuilderPool`,一定程度上缓解GC和内存占用 diff --git a/src/LightORM/ExceptionPolyfills.cs b/src/LightORM/ExceptionPolyfills.cs index 56ceca60..b199e04e 100644 --- a/src/LightORM/ExceptionPolyfills.cs +++ b/src/LightORM/ExceptionPolyfills.cs @@ -16,9 +16,25 @@ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpres } } + extension(ObjectDisposedException) + { + public static void ThrowIf(bool condition, object instance) + { + if (condition) + ThrowObjectDisposedException(instance); + } + + } + [DoesNotReturn] internal static void Throw(string? paramName) => throw new ArgumentNullException(paramName); + + [DoesNotReturn] + internal static void ThrowObjectDisposedException(object? instance) + { + throw new ObjectDisposedException(instance?.GetType().FullName); + } } } diff --git a/src/LightORM/ExpressionSql/DbInitial.cs b/src/LightORM/ExpressionSql/DbInitial.cs index b1e01b27..5febb232 100644 --- a/src/LightORM/ExpressionSql/DbInitial.cs +++ b/src/LightORM/ExpressionSql/DbInitial.cs @@ -2,7 +2,7 @@ namespace LightORM.ExpressionSql; -public class DbInitial(ISqlExecutor executor, IDatabaseTableHandler handler) : IDbInitial +public class DbInitial(SqlAdo executor, IDatabaseTableHandler handler) : IDbInitial { public IDbInitial CreateTable(params T[]? datas) { diff --git a/src/LightORM/ExpressionSql/DbInitialContext.cs b/src/LightORM/ExpressionSql/DbInitialContext.cs index 105766d8..3bf8e38f 100644 --- a/src/LightORM/ExpressionSql/DbInitialContext.cs +++ b/src/LightORM/ExpressionSql/DbInitialContext.cs @@ -4,12 +4,13 @@ namespace LightORM; public abstract class DbInitialContext { - //internal static MethodInfo InitializedMethod = typeof(ExpressionContext).GetMethod(nameof(ExpressionContext.Initialized))!; public abstract void Initialized(IDbInitial db); public virtual string DatabaseKey() => ConstString.Main; public DbInfo? Info { get; set; } + internal void Check(ExpressionSqlOptions option) { + var factory = new ConnectionFactory(option); bool hasTable = true; bool update = false; var key = DatabaseKey(); @@ -17,7 +18,8 @@ internal void Check(ExpressionSqlOptions option) { throw new LightOrmException($"{key} not register"); } - using var executor = new SqlExecutor.SqlExecutor(db, new AdoInterceptor(option.Interceptors)); + using var connection = factory.GetDatabaseConnection(key); + var executor = new SqlAdo(connection); option.DatabaseHandlers.TryGetValue(key, out var handler); if (handler is null) { diff --git a/src/LightORM/ExpressionSql/ExpressionCoreSql.Tran.cs b/src/LightORM/ExpressionSql/ExpressionCoreSql.Tran.cs index 28df5733..8c90190d 100644 --- a/src/LightORM/ExpressionSql/ExpressionCoreSql.Tran.cs +++ b/src/LightORM/ExpressionSql/ExpressionCoreSql.Tran.cs @@ -6,16 +6,16 @@ partial class ExpressionCoreSql : IExpressionContext { public ISingleScopedExpressionContext Use(IDatabaseProvider db) { - // 确保Use之后,拿到的ISqlExecutor是对应的 - var ado = new SqlExecutor.SqlExecutor(db, Options.PoolSize, new AdoInterceptor(Options.Interceptors)); - return new SingleScopedExpressionCoreSql(ado, Options); + + var connection = connectionFactory.GetDatabaseConnection(db); + return new SingleScopedExpressionCoreSql(connection, Options); } public ISingleScopedExpressionContext CreateScoped(string key) { Debug.WriteLine("CreateScoped"); - var ado = (ISqlExecutor)executorProvider.GetSqlExecutor(key).Clone(); - return new SingleScopedExpressionCoreSql(ado, Options); + var connection = connectionFactory.GetDatabaseConnection(key); + return new SingleScopedExpressionCoreSql(connection, Options); } public IScopedExpressionContext CreateScoped() diff --git a/src/LightORM/ExpressionSql/ExpressionCoreSql.cs b/src/LightORM/ExpressionSql/ExpressionCoreSql.cs index f97852c5..a7b30ee2 100644 --- a/src/LightORM/ExpressionSql/ExpressionCoreSql.cs +++ b/src/LightORM/ExpressionSql/ExpressionCoreSql.cs @@ -1,26 +1,29 @@ -using System.Threading; +using System.Collections.Concurrent; +using System.Threading; using LightORM.DbStruct; namespace LightORM.ExpressionSql; -internal sealed partial class ExpressionCoreSql(ExpressionSqlOptions option) : ExpressionCoreSqlBase, IExpressionContext +internal sealed partial class ExpressionCoreSql(ExpressionSqlOptions option) : ExpressionCoreSqlBase(option), IExpressionContext { - public override ExpressionSqlOptions Options { get; } = option; - internal readonly SqlExecutorProvider executorProvider = new(option); + //private readonly ConcurrentDictionary connections = []; + private readonly ConnectionFactory connectionFactory = new(option); public string Id { get; } = $"{Guid.NewGuid():N}"; - public override ISqlExecutor Ado => executorProvider.GetSqlExecutor(Options.DefaultDbKey); - - public ITransientExpressionContext SwitchDatabase(string key) + public override SqlAdo Ado => GetAdo(Options.DefaultDbKey); + public TransientExpressionContext SwitchDatabase(string key) { - var ado = executorProvider.GetSqlExecutor(key); - return TransientExpressionCoreSql.Create(key, ado, Options); + var connection = connectionFactory.GetDatabaseConnection(key); + return new(this, connection, Options); } - public ISqlExecutor GetAdo(string key) => executorProvider.GetSqlExecutor(key); + public SqlAdo GetAdo(string key) + { + var connection = connectionFactory.GetDatabaseConnection(key); + return new(connection); + } public IExpSelect Select(string tableName) => throw new NotImplementedException(); //new SelectProvider0(tableName, Ado); - private bool disposedValue; private void Dispose(bool disposing) @@ -30,7 +33,10 @@ private void Dispose(bool disposing) if (disposing) { System.Diagnostics.Debug.WriteLine($"释放ExpressionCoreSql:{DateTime.Now}"); - executorProvider.Dispose(); + //foreach (var item in connections.Values) + //{ + // item.Dispose(); + //} } disposedValue = true; @@ -45,25 +51,28 @@ public void Dispose() public string? CreateTableSql(IDatabaseProvider provider, Action? action = null) { - using var ado = new SqlExecutor.SqlExecutor(provider, Options.PoolSize, new(Options.Interceptors)); - return InternalCreateTableSql(ado, Options, action); + return ExpressionCoreSqlContextMethodImpl.InternalCreateTableSql(provider, action); } public async Task CreateTableAsync(IDatabaseProvider provider, Action? action = null, CancellationToken cancellationToken = default) { - using var ado = new SqlExecutor.SqlExecutor(provider, Options.PoolSize, new(Options.Interceptors)); - return await InternalCreateTableAsync(ado, Options, action, cancellationToken); + using var connection = connectionFactory.GetDatabaseConnection(provider); + var ado = new SqlAdo(connection); + var flag = await ExpressionCoreSqlContextMethodImpl.InternalCreateTableAsync(ado, Options, action, cancellationToken); + return flag; } public async Task> GetTablesAsync(IDatabaseProvider provider) { - using var ado = new SqlExecutor.SqlExecutor(provider, Options.PoolSize, new(Options.Interceptors)); - return await InternalGetTablesAsync(ado, Options); + using var connection = connectionFactory.GetDatabaseConnection(provider); + var ado = new SqlAdo(connection); + return await ExpressionCoreSqlContextMethodImpl.InternalGetTablesAsync(ado, Options); } public async Task GetTableStructAsync(IDatabaseProvider provider, DbStruct.ReadedTable table) { - using var ado = new SqlExecutor.SqlExecutor(provider, Options.PoolSize, new(Options.Interceptors)); - return await InternalTableStructAsync(table, ado, Options); + using var connection = connectionFactory.GetDatabaseConnection(provider); + var ado = new SqlAdo(connection); + return await ExpressionCoreSqlContextMethodImpl.InternalTableStructAsync(table, ado, Options); } } \ No newline at end of file diff --git a/src/LightORM/ExpressionSql/ExpressionCoreSqlBase.cs b/src/LightORM/ExpressionSql/ExpressionCoreSqlBase.cs index 3e901781..cc9eff87 100644 --- a/src/LightORM/ExpressionSql/ExpressionCoreSqlBase.cs +++ b/src/LightORM/ExpressionSql/ExpressionCoreSqlBase.cs @@ -5,175 +5,44 @@ namespace LightORM.ExpressionSql; -internal abstract class ExpressionCoreSqlBase : IContext +internal abstract class ExpressionCoreSqlBase(ExpressionSqlOptions options) : IContext { - public abstract ISqlExecutor Ado { get; } - public abstract ExpressionSqlOptions Options { get; } + public abstract SqlAdo Ado { get; } + public ExpressionSqlOptions Options { get; } = options; public MultipleResult QueryMultiple(params IExpSelect[] selects) - { - if (selects.Length == 0) - { - throw new LightOrmException("selects 数量为0"); - } -#if NET8_0_OR_GREATER - string[] sqls = System.Buffers.ArrayPool.Shared.Rent(selects.Length); -#else - string[] sqls = new string[selects.Length]; -#endif - Dictionary parameters = []; - try - { - for (var i = 0; i < selects.Length; i++) - { - var select = selects[i]; - var originSql = select.SqlBuilder.ToSqlString(Ado.Database.DatabaseAdapter); - - if (select.SqlBuilder.DbParameters.Count > 0) - { - sqls[i] = Ado.Database.DatabaseAdapter.RewriteParameterReferences(originSql, $"q{i}"); - - foreach (var item in select.SqlBuilder.DbParameters) - { - parameters[$"q{i}_{item.Key}"] = item.Value; - } - } - else - { - sqls[i] = originSql; - } - } - var sql = Ado.Database.DatabaseAdapter.HandleMultipleQuerySql(sqls, parameters); - var reader = Ado.ExecuteReader(sql, parameters); - return new MultipleResult(reader); - } - finally - { -#if NET8_0_OR_GREATER - System.Buffers.ArrayPool.Shared.Return(sqls); -#endif - } - } - - public async Task QueryMultipleAsync(IExpSelect[] selects, CancellationToken cancellationToken = default) - { - if (selects.Length == 0) - { - throw new LightOrmException("selects 数量为0"); - } -#if NET8_0_OR_GREATER - string[] sqls = System.Buffers.ArrayPool.Shared.Rent(selects.Length); -#else - string[] sqls = new string[selects.Length]; -#endif - Dictionary parameters = []; - try - { - for (var i = 0; i < selects.Length; i++) - { - var select = selects[i]; - var originSql = select.SqlBuilder.ToSqlString(Ado.Database.DatabaseAdapter); + => ExpressionCoreSqlContextMethodImpl.QueryMultiple(Ado, selects); - if (select.SqlBuilder.DbParameters.Count > 0) - { - sqls[i] = Ado.Database.DatabaseAdapter.RewriteParameterReferences(originSql, $"q{i}"); - - foreach (var item in select.SqlBuilder.DbParameters) - { - parameters[$"q{i}_{item.Key}"] = item.Value; - } - } - else - { - sqls[i] = originSql; - } - } - var sql = Ado.Database.DatabaseAdapter.HandleMultipleQuerySql(sqls, parameters); - var reader = await Ado.ExecuteReaderAsync(sql, parameters, cancellationToken: cancellationToken); - return new MultipleResult(reader); - } - finally - { -#if NET8_0_OR_GREATER - System.Buffers.ArrayPool.Shared.Return(sqls); -#endif - } - } + public Task QueryMultipleAsync(IExpSelect[] selects, CancellationToken cancellationToken = default) + => ExpressionCoreSqlContextMethodImpl.QueryMultipleAsync(Ado, selects, cancellationToken); public IExpSelect Select< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>() => new SelectProvider1(this); + T>() => ExpressionCoreSqlContextMethodImpl.Select(this); #region insert - public IExpInsert Insert() => CreateInsertProvider(); + public IExpInsert Insert() => ExpressionCoreSqlContextMethodImpl.Insert(Ado); - public IExpInsert Insert(params T[] entities) - { - if (entities.Length == 0) - { - return CreateInsertProvider(default(T)); - } - if (entities.Length == 1) - { - return CreateInsertProvider(entities[0]); - } - else - { - return CreateInsertProvider(entities); - } - } - - InsertProvider CreateInsertProvider(T? entity = default) => new(Ado, entity); - InsertProvider CreateInsertProvider(T[] entities) => new(Ado, entities); + public IExpInsert Insert(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Insert(Ado, entities); #endregion #region update - public IExpUpdate Update() => CreateUpdateProvider(); + public IExpUpdate Update() => ExpressionCoreSqlContextMethodImpl.Update(Ado); - public IExpUpdate Update(params T[] entities) - { - if (entities.Length == 0) - { - return CreateUpdateProvider(default(T)); - } - if (entities.Length == 1) - { - return CreateUpdateProvider(entities[0]); - } - else - { - return CreateUpdateProvider(entities); - } - } - - UpdateProvider CreateUpdateProvider(T? entity = default) => new(Ado, entity); - UpdateProvider CreateUpdateProvider(T[] entities) => new(Ado, entities); + public IExpUpdate Update(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Update(Ado, entities); #endregion #region delete - public IExpDelete Delete() => CreateDeleteProvider(); - - public IExpDelete Delete(params T[] entities) - { - if (entities.Length == 1) - { - return CreateDeleteProvider(entities[0]); - } - else - { - return CreateDeleteProvider(entities); - } - } + public IExpDelete Delete() => ExpressionCoreSqlContextMethodImpl.Delete(Ado); - DeleteProvider CreateDeleteProvider(T? entity = default) => new(Ado, entity); - DeleteProvider CreateDeleteProvider(T[] entities) => new(Ado, entities); + public IExpDelete Delete(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Delete(Ado, entities); #endregion @@ -182,112 +51,32 @@ public IExpDelete Delete(params T[] entities) public string? CreateTableSql(Action? action = null) { var ado = Ado; - return InternalCreateTableSql(ado, Options, action); + return ExpressionCoreSqlContextMethodImpl.InternalCreateTableSql(ado.Provider, action); } public async Task CreateTableAsync(Action? action = null, CancellationToken cancellationToken = default) { var ado = Ado; - return await InternalCreateTableAsync(ado, Options, action, cancellationToken); + return await ExpressionCoreSqlContextMethodImpl.InternalCreateTableAsync(ado, Options, action, cancellationToken); } public async Task> GetTablesAsync() { var ado = Ado; - return await InternalGetTablesAsync(ado, Options); + return await ExpressionCoreSqlContextMethodImpl.InternalGetTablesAsync(ado, Options); } public async Task GetTableStructAsync(DbStruct.ReadedTable table) { var ado = Ado; - return await InternalTableStructAsync(table, ado, Options); + return await ExpressionCoreSqlContextMethodImpl.InternalTableStructAsync(table, ado, Options); } public async Task DropTableAsync(CancellationToken cancellationToken = default) { var ado = Ado; var t = TableContext.GetTableInfo(); - return await InternalDropTableAsync(ado, t.TableName, cancellationToken); - } - - protected static string InternalCreateTableSql(ISqlExecutor ado, ExpressionSqlOptions option, Action? action = null) - { - try - { - return string.Join(Environment.NewLine, GenerateDbTable(ado, option, action)); - } - catch (Exception) - { - throw; - } - } - - protected static async Task InternalCreateTableAsync(ISqlExecutor ado, ExpressionSqlOptions options, Action? action, CancellationToken cancellationToken) - { - try - { - var sqls = GenerateDbTable(ado, options, action).ToArray(); - if (sqls.Length == 0) - { - return false; - } - - ado.BeginTransaction(); - foreach (var s in sqls) - { - await ado.ExecuteNonQueryAsync(s, cancellationToken: cancellationToken); - } - - await ado.CommitTransactionAsync(cancellationToken); - return true; - } - catch (Exception) - { - await ado.RollbackTransactionAsync(cancellationToken); - return false; - } - } - - protected static async Task> InternalGetTablesAsync(ISqlExecutor ado, ExpressionSqlOptions _) - { - if (ado.Database.DbHandler is null) - return []; - var sql = ado.Database.DbHandler.GetTablesSql(); - return await ado.Execute(sql).ToListAsync(); - } - - protected static async Task InternalTableStructAsync(ReadedTable table, ISqlExecutor ado, ExpressionSqlOptions _) - { - if (ado.Database.DbHandler is null) - throw new NotSupportedException(); - var sql = ado.Database.DbHandler.GetTableStructSql(table.TableName!); - var columns = await ado.Execute(sql).ToListAsync(); - return table with { Columns = columns }; - } - - protected static async Task InternalDropTableAsync(ISqlExecutor ado, string tableName, CancellationToken cancellationToken) - { - try - { - if (ado.Database.DbHandler is null) - throw new NotSupportedException(); - var sql = ado.Database.DbHandler.GetDropTableSql(tableName); - await ado.ExecuteNonQueryAsync(sql, cancellationToken: cancellationToken); - return true; - } - catch - { - return false; - } - - } - - private static IEnumerable GenerateDbTable(ISqlExecutor ado, ExpressionSqlOptions option, Action? action = null) - { - if (ado.Database.DbHandler is null) - return []; - var tableSql = ado.Database.DbHandler.GenerateDbTable(); - return tableSql; + return await ExpressionCoreSqlContextMethodImpl.InternalDropTableAsync(ado, t.TableName, cancellationToken); } #endregion diff --git a/src/LightORM/ExpressionSql/ExpressionCoreSqlContextMethodImpl.cs b/src/LightORM/ExpressionSql/ExpressionCoreSqlContextMethodImpl.cs new file mode 100644 index 00000000..d9d9c00a --- /dev/null +++ b/src/LightORM/ExpressionSql/ExpressionCoreSqlContextMethodImpl.cs @@ -0,0 +1,260 @@ +using LightORM.DbStruct; +using LightORM.Providers; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace LightORM.ExpressionSql; + +internal static class ExpressionCoreSqlContextMethodImpl +{ + public static MultipleResult QueryMultiple(SqlAdo ado, params IExpSelect[] selects) + { + if (selects.Length == 0) + { + throw new LightOrmException("selects 数量为0"); + } +#if NET8_0_OR_GREATER + string[] sqls = System.Buffers.ArrayPool.Shared.Rent(selects.Length); +#else + string[] sqls = new string[selects.Length]; +#endif + Dictionary parameters = []; + try + { + for (var i = 0; i < selects.Length; i++) + { + var select = selects[i]; + var originSql = select.SqlBuilder.ToSqlString(ado.Provider.DatabaseAdapter); + + if (select.SqlBuilder.DbParameters.Count > 0) + { + sqls[i] = ado.Provider.DatabaseAdapter.RewriteParameterReferences(originSql, $"q{i}"); + + foreach (var item in select.SqlBuilder.DbParameters) + { + parameters[$"q{i}_{item.Key}"] = item.Value; + } + } + else + { + sqls[i] = originSql; + } + } + var sql = ado.Provider.DatabaseAdapter.HandleMultipleQuerySql(sqls, parameters); + var reader = ado.ExecuteReader(sql, parameters); + return new MultipleResult(reader); + } + finally + { +#if NET8_0_OR_GREATER + System.Buffers.ArrayPool.Shared.Return(sqls); +#endif + } + } + + public static async Task QueryMultipleAsync(SqlAdo ado, IExpSelect[] selects, CancellationToken cancellationToken = default) + { + if (selects.Length == 0) + { + throw new LightOrmException("selects 数量为0"); + } +#if NET8_0_OR_GREATER + string[] sqls = System.Buffers.ArrayPool.Shared.Rent(selects.Length); +#else + string[] sqls = new string[selects.Length]; +#endif + Dictionary parameters = []; + try + { + for (var i = 0; i < selects.Length; i++) + { + var select = selects[i]; + var originSql = select.SqlBuilder.ToSqlString(ado.Provider.DatabaseAdapter); + + if (select.SqlBuilder.DbParameters.Count > 0) + { + sqls[i] = ado.Provider.DatabaseAdapter.RewriteParameterReferences(originSql, $"q{i}"); + + foreach (var item in select.SqlBuilder.DbParameters) + { + parameters[$"q{i}_{item.Key}"] = item.Value; + } + } + else + { + sqls[i] = originSql; + } + } + var sql = ado.Provider.DatabaseAdapter.HandleMultipleQuerySql(sqls, parameters); + var reader = await ado.ExecuteReaderAsync(sql, parameters, cancellationToken: cancellationToken); + return new MultipleResult(reader); + } + finally + { +#if NET8_0_OR_GREATER + System.Buffers.ArrayPool.Shared.Return(sqls); +#endif + } + } + + public static IExpSelect Select< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + T>(IContext context) => new SelectProvider1(context); + + #region insert + + public static IExpInsert Insert(SqlAdo ado) => CreateInsertProvider(ado); + + public static IExpInsert Insert(SqlAdo ado, params T[] entities) + { + if (entities.Length == 0) + { + return CreateInsertProvider(ado, default(T)); + } + if (entities.Length == 1) + { + return CreateInsertProvider(ado, entities[0]); + } + else + { + return CreateInsertProvider(ado, entities); + } + } + + static InsertProvider CreateInsertProvider(SqlAdo ado, T? entity = default) => new(ado, entity); + static InsertProvider CreateInsertProvider(SqlAdo ado, T[] entities) => new(ado, entities); + + #endregion + + #region update + + public static IExpUpdate Update(SqlAdo ado) => CreateUpdateProvider(ado); + + public static IExpUpdate Update(SqlAdo ado, params T[] entities) + { + if (entities.Length == 0) + { + return CreateUpdateProvider(ado, default(T)); + } + if (entities.Length == 1) + { + return CreateUpdateProvider(ado, entities[0]); + } + else + { + return CreateUpdateProvider(ado, entities); + } + } + + static UpdateProvider CreateUpdateProvider(SqlAdo ado, T? entity = default) => new(ado, entity); + static UpdateProvider CreateUpdateProvider(SqlAdo ado, T[] entities) => new(ado, entities); + + #endregion + + #region delete + + public static IExpDelete Delete(SqlAdo ado) => CreateDeleteProvider(ado); + + public static IExpDelete Delete(SqlAdo ado, params T[] entities) + { + if (entities.Length == 1) + { + return CreateDeleteProvider(ado, entities[0]); + } + else + { + return CreateDeleteProvider(ado, entities); + } + } + + static DeleteProvider CreateDeleteProvider(SqlAdo ado, T? entity = default) => new(ado, entity); + static DeleteProvider CreateDeleteProvider(SqlAdo ado, T[] entities) => new(ado, entities); + + #endregion + + #region 数据库表操作 + + public static string InternalCreateTableSql(IDatabaseProvider provider, Action? action = null) + { + try + { + return string.Join(Environment.NewLine, GenerateDbTable(provider, action)); + } + catch (Exception) + { + throw; + } + } + + public static async Task InternalCreateTableAsync(SqlAdo ado, ExpressionSqlOptions options, Action? action, CancellationToken cancellationToken) + { + try + { + var sqls = GenerateDbTable(ado.Provider, action).ToArray(); + if (sqls.Length == 0) + { + return false; + } + + //ado.BeginTransaction(); + foreach (var s in sqls) + { + await ado.ExecuteNonQueryAsync(s, cancellationToken: cancellationToken); + } + + //await ado.CommitTransactionAsync(cancellationToken); + return true; + } + catch (Exception) + { + //await ado.RollbackTransactionAsync(cancellationToken); + return false; + } + } + + public static async Task> InternalGetTablesAsync(SqlAdo ado, ExpressionSqlOptions _) + { + if (ado.Provider.DbHandler is null) + return []; + var sql = ado.Provider.DbHandler.GetTablesSql(); + return await ado.Execute(sql).ToListAsync(); + } + + public static async Task InternalTableStructAsync(ReadedTable table, SqlAdo ado, ExpressionSqlOptions _) + { + if (ado.Provider.DbHandler is null) + throw new NotSupportedException(); + var sql = ado.Provider.DbHandler.GetTableStructSql(table.TableName!); + var columns = await ado.Execute(sql).ToListAsync(); + return table with { Columns = columns }; + } + + public static async Task InternalDropTableAsync(SqlAdo ado, string tableName, CancellationToken cancellationToken) + { + try + { + if (ado.Provider.DbHandler is null) + throw new NotSupportedException(); + var sql = ado.Provider.DbHandler.GetDropTableSql(tableName); + await ado.ExecuteNonQueryAsync(sql, cancellationToken: cancellationToken); + return true; + } + catch + { + return false; + } + + } + + private static IEnumerable GenerateDbTable(IDatabaseProvider provider, Action? action = null) + { + if (provider.DbHandler is null) + return []; + var tableSql = provider.DbHandler.GenerateDbTable(); + return tableSql; + } + + #endregion +} diff --git a/src/LightORM/ExpressionSql/ScopedExpressionCoreSql.cs b/src/LightORM/ExpressionSql/ScopedExpressionCoreSql.cs index 1eb43bb4..44ac4300 100644 --- a/src/LightORM/ExpressionSql/ScopedExpressionCoreSql.cs +++ b/src/LightORM/ExpressionSql/ScopedExpressionCoreSql.cs @@ -1,68 +1,54 @@ -using System.Threading; -using static LightORM.SqlExecutor.SqlExecutor; +using System.Collections.Concurrent; +using System.Threading; namespace LightORM.ExpressionSql; -internal sealed class ScopedExpressionCoreSql : ExpressionCoreSqlBase, IScopedExpressionContext +internal sealed class ScopedExpressionCoreSql(ExpressionSqlOptions options) : ExpressionCoreSqlBase(options), IScopedExpressionContext { - private readonly SqlExecutorProvider executorProvider; - public string Id { get; } = $"{Guid.NewGuid():N}"; private bool useTrans; private IsolationLevel isolationLevel = IsolationLevel.Unspecified; - public override ExpressionSqlOptions Options { get; } - private TransientExpressionCoreSql? current; - public override ISqlExecutor Ado + private readonly ConnectionFactory connectionFactory = new(options); + private readonly ConcurrentDictionary connections = []; + private TransientExpressionContext? current; + public override SqlAdo Ado { get { - var ado = current?.Ado ?? DefaultAdo; - if (useTrans) + if (current.HasValue) { - ado.InitTransaction(isolationLevel); + return current.Value.Ado; } - return ado; + return DefaultAdo; } } - public ISqlExecutor DefaultAdo + public SqlAdo DefaultAdo { get { - var ado = executorProvider.GetSqlExecutor(Options.DefaultDbKey); + var connection = connections.GetOrAdd(Options.DefaultDbKey, connectionFactory.GetDatabaseConnection); if (useTrans) { - ado.InitTransaction(isolationLevel); + connection.BeginTransaction(); } - return ado; + return new(connection); } } - - public ScopedExpressionCoreSql(ExpressionSqlOptions options) - { - this.executorProvider = new SqlExecutorProvider(options); - Options = options; - foreach (var item in options.DatabaseProviders.Values) - { - var ctx = AsyncLocalTransactionContexts.GetOrAdd(item, new AsyncLocal()); - ctx.Value ??= new TransactionContext(); - } - } - - private readonly Dictionary contextCaches = []; - ITransientExpressionContext IScopedExpressionContext.SwitchDatabase(string key) + private readonly Dictionary contextCaches = []; + TransientExpressionContext IScopedExpressionContext.SwitchDatabase(string key) { if (contextCaches.TryGetValue(key, out var ctx)) { return ctx; } - var ado = executorProvider.GetSqlExecutor(key); + var connection = connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection); if (useTrans) { - ado.InitTransaction(isolationLevel); + connection.BeginTransaction(isolationLevel); } - ctx = new(key, ado, Options); + ctx = new(this, connection, Options); contextCaches[key] = ctx; current = ctx; return ctx; @@ -70,39 +56,85 @@ ITransientExpressionContext IScopedExpressionContext.SwitchDatabase(string key) public void Dispose() { - executorProvider.Dispose(); + foreach (var item in connections.Values) + { + item.Dispose(); + } } public void BeginAllTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) { useTrans = true; this.isolationLevel = isolationLevel; - executorProvider.Executors.ForEach(e => e.BeginTransaction(isolationLevel)); + foreach (var item in connections.Values) + { + item.BeginTransaction(isolationLevel); + } + } + public void CommitAllTransaction() + { + foreach (var item in connections.Values) + { + item.CommitTransaction(); + } } + public void RollbackAllTransaction() + { + foreach (var item in connections.Values) + { + item.RollbackTransaction(); + } + } + public void BeginTransaction(string key = "MainDb", IsolationLevel isolationLevel = IsolationLevel.Unspecified) + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).BeginTransaction(isolationLevel); + + public void CommitTransaction(string key = "MainDb") + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).CommitTransaction(); - public async Task BeginAllTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + public void RollbackTransaction(string key = "MainDb") + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).RollbackTransaction(); + +#if NET8_0_OR_GREATER + public async Task BeginAllTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default) { useTrans = true; this.isolationLevel = isolationLevel; - await executorProvider.Executors.ForEachAsync(e => e.BeginTransactionAsync(isolationLevel)); + foreach (var item in connections.Values) + { + await item.BeginTransactionAsync(isolationLevel, cancellationToken); + } + } + + public async Task CommitAllTransactionAsync(CancellationToken cancellationToken = default) + { + foreach (var item in connections.Values) + { + await item.CommitTransactionAsync(cancellationToken); + } } - public void CommitAllTransaction() => executorProvider.Executors.ForEach(e => e.CommitTransaction()); - public async Task CommitAllTransactionAsync() => await executorProvider.Executors.ForEachAsync(e => e.CommitTransactionAsync()); - public void RollbackAllTransaction() => executorProvider.Executors.ForEach(e => e.RollbackTransaction()); + public async Task RollbackAllTransactionAsync(CancellationToken cancellationToken = default) + { + foreach (var item in connections.Values) + { + await item.RollbackTransactionAsync(cancellationToken); + } + } - public async Task RollbackAllTransactionAsync() => await executorProvider.Executors.ForEachAsync(e => e.RollbackTransactionAsync()); - public void BeginTransaction(string key = "MainDb", IsolationLevel isolationLevel = IsolationLevel.Unspecified) => executorProvider.GetSqlExecutor(key).BeginTransaction(); + public Task BeginTransactionAsync(string key = "MainDb" + , IsolationLevel isolationLevel = IsolationLevel.Unspecified + , CancellationToken cancellationToken = default) + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).BeginTransactionAsync(isolationLevel, cancellationToken); - public Task BeginTransactionAsync(string key = "MainDb", IsolationLevel isolationLevel = IsolationLevel.Unspecified) => executorProvider.GetSqlExecutor(key).BeginTransactionAsync(); - public void CommitTransaction(string key = "MainDb") => executorProvider.GetSqlExecutor(key).CommitTransaction(); + public Task CommitTransactionAsync(string key = "MainDb", CancellationToken cancellationToken = default) + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).CommitTransactionAsync(cancellationToken); - public Task CommitTransactionAsync(string key = "MainDb") => executorProvider.GetSqlExecutor(key).CommitTransactionAsync(); - public void RollbackTransaction(string key = "MainDb") => executorProvider.GetSqlExecutor(key).RollbackTransaction(); + public Task RollbackTransactionAsync(string key = "MainDb", CancellationToken cancellationToken = default) + => connections.GetOrAdd(key, connectionFactory.GetDatabaseConnection).RollbackTransactionAsync(cancellationToken); +#endif - public Task RollbackTransactionAsync(string key = "MainDb") => executorProvider.GetSqlExecutor(key).RollbackTransactionAsync(); } diff --git a/src/LightORM/ExpressionSql/SingleScopedExpressionCoreSql.cs b/src/LightORM/ExpressionSql/SingleScopedExpressionCoreSql.cs index ffadd281..da0da7b1 100644 --- a/src/LightORM/ExpressionSql/SingleScopedExpressionCoreSql.cs +++ b/src/LightORM/ExpressionSql/SingleScopedExpressionCoreSql.cs @@ -3,39 +3,48 @@ namespace LightORM.ExpressionSql; -internal sealed class SingleScopedExpressionCoreSql : ExpressionCoreSqlBase, ISingleScopedExpressionContext +internal sealed class SingleScopedExpressionCoreSql(DatabaseConnection databaseConnection, ExpressionSqlOptions options) : ExpressionCoreSqlBase(options), ISingleScopedExpressionContext { public string Id { get; } = $"{Guid.NewGuid():N}"; - public bool IsTransaction { get; set; } + public override SqlAdo Ado => new(databaseConnection); - public override ExpressionSqlOptions Options { get; } - public SingleScopedExpressionCoreSql(ISqlExecutor sqlExecutor, ExpressionSqlOptions options) - { - Ado = sqlExecutor; - Options = options; - Ado.InitTransactionContext(); - } - public override ISqlExecutor Ado { get; } - - - public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) => Ado.BeginTransaction(isolationLevel); + public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + => databaseConnection.BeginTransaction(isolationLevel); + public void CommitTransaction() => databaseConnection.CommitTransaction(); + public void RollbackTransaction() => databaseConnection.RollbackTransaction(); - public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified) => await Ado.BeginTransactionAsync(isolationLevel).ConfigureAwait(false); +#if NET8_0_OR_GREATER - public void CommitTransaction() => Ado.CommitTransaction(); + public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified + , CancellationToken cancellationToken = default) + => await databaseConnection.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false); - public async Task CommitTransactionAsync() => await Ado.CommitTransactionAsync().ConfigureAwait(false); - - public void RollbackTransaction() => Ado.RollbackTransaction(); - - public async Task RollbackTransactionAsync() => await Ado.RollbackTransactionAsync().ConfigureAwait(false); + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + => await databaseConnection.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + => await databaseConnection.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#endif public void Dispose() { - Ado.Dispose(); + if (databaseConnection.State == AdoState.Active && databaseConnection.UnderTransaction) + { + try + { + CommitTransaction(); + } + catch (Exception) + { + RollbackTransaction(); + } + } + else + { + databaseConnection.Dispose(); + } } /// /// @@ -54,7 +63,7 @@ void ISingleScopedExpressionContext.TryBeginTransaction() { if (Interlocked.CompareExchange(ref transactionState, 1, 0) == 0) { - Ado.BeginTransaction(); + databaseConnection.BeginTransaction(); } } @@ -64,11 +73,11 @@ void ISingleScopedExpressionContext.TryCommitTransaction() { try { - Ado.CommitTransaction(); + databaseConnection.CommitTransaction(); } catch (Exception) { - Ado.RollbackTransaction(); + databaseConnection.RollbackTransaction(); throw; } } @@ -78,7 +87,7 @@ void ISingleScopedExpressionContext.TryRollbackTransaction() { if (Interlocked.CompareExchange(ref transactionState, 2, 1) == 1) { - Ado.RollbackTransaction(); + databaseConnection.RollbackTransaction(); } } diff --git a/src/LightORM/ExpressionSql/TransientExpressionCoreSql.cs b/src/LightORM/ExpressionSql/TransientExpressionCoreSql.cs index 5574bf59..b97b3b80 100644 --- a/src/LightORM/ExpressionSql/TransientExpressionCoreSql.cs +++ b/src/LightORM/ExpressionSql/TransientExpressionCoreSql.cs @@ -1,17 +1,108 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace LightORM.ExpressionSql; -internal sealed class TransientExpressionCoreSql(string key, ISqlExecutor ado, ExpressionSqlOptions options) : ExpressionCoreSqlBase, ITransientExpressionContext +public readonly struct TransientExpressionContext +{ + private readonly DatabaseConnection connection; + + public SqlAdo Ado => new(connection); + internal ExpressionSqlOptions Options { get; } + internal IContext Context { get; } + + internal TransientExpressionContext(IContext context, DatabaseConnection connection, ExpressionSqlOptions options) + { + Options = options; + Context = context; + this.connection = connection; + } + + public MultipleResult QueryMultiple(params IExpSelect[] selects) + => ExpressionCoreSqlContextMethodImpl.QueryMultiple(Ado, selects); + + public Task QueryMultipleAsync(IExpSelect[] selects, CancellationToken cancellationToken = default) + => ExpressionCoreSqlContextMethodImpl.QueryMultipleAsync(Ado, selects, cancellationToken); + + public IExpSelect Select< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + T>() => ExpressionCoreSqlContextMethodImpl.Select(Context); + + #region insert + + public IExpInsert Insert() => ExpressionCoreSqlContextMethodImpl.Insert(Ado); + + public IExpInsert Insert(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Insert(Ado, entities); + + #endregion + + #region update + + public IExpUpdate Update() => ExpressionCoreSqlContextMethodImpl.Update(Ado); + + public IExpUpdate Update(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Update(Ado, entities); + + #endregion + + #region delete + + public IExpDelete Delete() => ExpressionCoreSqlContextMethodImpl.Delete(Ado); + + public IExpDelete Delete(params T[] entities) => ExpressionCoreSqlContextMethodImpl.Delete(Ado, entities); + + #endregion + + #region 数据库表操作 + + public string? CreateTableSql(Action? action = null) + { + var ado = Ado; + return ExpressionCoreSqlContextMethodImpl.InternalCreateTableSql(ado.Provider, action); + } + + public async Task CreateTableAsync(Action? action = null, CancellationToken cancellationToken = default) + { + var ado = Ado; + return await ExpressionCoreSqlContextMethodImpl.InternalCreateTableAsync(ado, Options, action, cancellationToken); + } + + public async Task> GetTablesAsync() + { + var ado = Ado; + return await ExpressionCoreSqlContextMethodImpl.InternalGetTablesAsync(ado, Options); + } + + public async Task GetTableStructAsync(DbStruct.ReadedTable table) + { + var ado = Ado; + return await ExpressionCoreSqlContextMethodImpl.InternalTableStructAsync(table, ado, Options); + } + + public async Task DropTableAsync(CancellationToken cancellationToken = default) + { + var ado = Ado; + var t = TableContext.GetTableInfo(); + return await ExpressionCoreSqlContextMethodImpl.InternalDropTableAsync(ado, t.TableName, cancellationToken); + } + + #endregion +} + +[Obsolete] +internal sealed class TransientExpressionCoreSql(string key + , DatabaseConnection connection + , ExpressionSqlOptions options) : ExpressionCoreSqlBase(options), ITransientExpressionContext { private static readonly ConcurrentDictionary> weakCache = new(); - - public override ISqlExecutor Ado { get; } = ado; - public override ExpressionSqlOptions Options { get; } = options; + public override SqlAdo Ado { get; } = new(connection); + public string Key { get; } = key; - public static TransientExpressionCoreSql Create(string key, ISqlExecutor executor, ExpressionSqlOptions options) + public static TransientExpressionCoreSql Create(string key, DatabaseConnection connection, ExpressionSqlOptions options) { // 尝试从缓存获取 if (weakCache.TryGetValue(key, out var weakRef)) @@ -23,7 +114,7 @@ public static TransientExpressionCoreSql Create(string key, ISqlExecutor executo } // 创建新实例并缓存 - var newInstance = new TransientExpressionCoreSql(key, executor, options); + var newInstance = new TransientExpressionCoreSql(key, connection, options); weakCache[key] = new WeakReference(newInstance); return newInstance; } diff --git a/src/LightORM/Extension/ExpressionContextExtension.cs b/src/LightORM/Extension/ExpressionContextExtension.cs index c88be453..8e100c07 100644 --- a/src/LightORM/Extension/ExpressionContextExtension.cs +++ b/src/LightORM/Extension/ExpressionContextExtension.cs @@ -2,13 +2,14 @@ using LightORM.Providers; using LightORM.Repository; using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace LightORM; public static class ExpressionContextExtension { /// - extension(ISqlExecutor ado) + extension(SqlAdo ado) { /// /// 批量插入 @@ -54,7 +55,7 @@ public int BulkCopy(IEnumerable datas) if (value is bool b) { // bool类型特殊处理 - row[col.ColumnName] = ado.Database.DatabaseAdapter.HandleBooleanValueForBulkCopy(b); + row[col.ColumnName] = ado.Provider.DatabaseAdapter.HandleBooleanValueForBulkCopy(b); continue; } @@ -75,7 +76,7 @@ public int BulkCopy(IEnumerable datas) /// public int BulkCopy(DataTable dataTable) { - return ado.Database.BulkCopy(dataTable); + return ado.Provider.BulkCopy(dataTable); } } @@ -85,7 +86,7 @@ public IExpSelect Select< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>(string tableName) + T>(string tableName) { return new SelectProvider1(tableName, context); } @@ -151,13 +152,13 @@ public ILightOrmRepository GetRepository< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - TEntity>() + TEntity>() where TEntity : class, new() { return new DefaultRepository(context); } - private ITransientExpressionContext SwitchDb() + private TransientExpressionContext SwitchDb() { var table = TableContext.GetTableInfo(); if (table.TargetDatabase is null) @@ -172,7 +173,7 @@ public IExpSelect SelectWithAttr< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>() + T>() => context.SwitchDb().Select(); public IExpInsert InsertWithAttr(T entity) @@ -263,7 +264,7 @@ public static class ScopedExpressionContextExtensions { extension(IScopedExpressionContext context) { - private ITransientExpressionContext SwitchDb() + private TransientExpressionContext SwitchDb() { var table = TableContext.GetTableInfo(); if (table.TargetDatabase is null) @@ -277,7 +278,7 @@ public IExpSelect SelectWithAttr< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>() + T>() => context.SwitchDb().Select(); public IExpInsert InsertWithAttr(T entity) @@ -303,5 +304,24 @@ public IExpDelete DeleteWithAttr(T entity) public IExpDelete DeleteWithAttr(params T[] entities) => context.SwitchDb().Delete(entities); + + public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + { + context.BeginTransaction(ConstString.Main, isolationLevel); + } +#if NET8_0_OR_GREATER + public Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default) + { + return context.BeginTransactionAsync(ConstString.Main, isolationLevel, cancellationToken); + } + public Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + return context.CommitTransactionAsync(ConstString.Main, cancellationToken); + } + public Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + return context.RollbackTransactionAsync(ConstString.Main, cancellationToken); + } +#endif } } \ No newline at end of file diff --git a/src/LightORM/Extension/GroupSelectExtensions.cs b/src/LightORM/Extension/GroupSelectExtensions.cs index 10d23ddd..01e46e88 100644 --- a/src/LightORM/Extension/GroupSelectExtensions.cs +++ b/src/LightORM/Extension/GroupSelectExtensions.cs @@ -11,7 +11,7 @@ public static IExpSelectGroup Result(this IExp public static DataTable ToDataTable(this IExpSelectGroup selectGroup) { - var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Database.DatabaseAdapter); + var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Provider.DatabaseAdapter); var dbParams = selectGroup.SqlBuilder.DbParameters; return selectGroup.Executor.ExecuteDataTable(sql, dbParams); } @@ -20,14 +20,14 @@ public static DataTable ToDataTable(this IExpSelectGroup ToDataTableAsync(this IExpSelectGroup selectGroup) { - var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Database.DatabaseAdapter); + var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Provider.DatabaseAdapter); var dbParams = selectGroup.SqlBuilder.DbParameters; return selectGroup.Executor.ExecuteDataTableAsync(sql, dbParams); } @@ -36,7 +36,7 @@ public static Task ToDataTableAsync(this IExpSelectG { var flatExp = FlatGrouping.Default.Flat(exp, selectGroup.KeySelector) ?? throw new LightOrmException("表达式扁平化失败"); selectGroup.SqlBuilder.Expressions.Add(new ExpressionInfo(SqlResolveOptions.Select, flatExp)); - var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Database.DatabaseAdapter); + var sql = selectGroup.SqlBuilder.ToSqlString(selectGroup.Executor.Provider.DatabaseAdapter); var dbParams = selectGroup.SqlBuilder.DbParameters; return selectGroup.Executor.ExecuteDataTableAsync(sql, dbParams); } diff --git a/src/LightORM/Extension/IncludeContextExtensions.cs b/src/LightORM/Extension/IncludeContextExtensions.cs index b2d034e8..6a19f5be 100644 --- a/src/LightORM/Extension/IncludeContextExtensions.cs +++ b/src/LightORM/Extension/IncludeContextExtensions.cs @@ -13,9 +13,9 @@ internal static class IncludeContextExtensions #if NET8_0_OR_GREATER #endif - public static void BindIncludeDatas(this SelectBuilder sqlBuilder, ISqlExecutor executor, object data) + public static void BindIncludeDatas(this SelectBuilder sqlBuilder, SqlAdo ado, object data) { - if (!AOTSupported) + if (IsAOTRuntime) { LightOrmException.Throw("当前配置不支持反射Include操作"); } @@ -26,7 +26,7 @@ public static void BindIncludeDatas(this SelectBuilder sqlBuilder, ISqlExecutor { foreach (IncludeInfo include in sqlBuilder.Includes) { - Do( executor, item, include); + Do(ado, item, include); } } } @@ -34,7 +34,7 @@ public static void BindIncludeDatas(this SelectBuilder sqlBuilder, ISqlExecutor { foreach (IncludeInfo include in sqlBuilder.Includes) { - Do( executor, data, include); + Do(ado, data, include); } } } @@ -43,15 +43,15 @@ public static void BindIncludeDatas(this SelectBuilder sqlBuilder, ISqlExecutor [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "这是反射路径")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "这是反射路径")] #endif - public static void Do(ISqlExecutor executor, object item, IncludeInfo include) + public static void Do(SqlAdo ado, object item, IncludeInfo include) { - var database = executor.Database.DatabaseAdapter; + var database = ado.Provider.DatabaseAdapter; SelectBuilder includeBuilder = BuildSql(database, include, item); var selectedType = include.NavigateInfo!.NavigateType; string sql = includeBuilder.ToSqlString(database); var param = includeBuilder.DbParameters; var typedQuery = QueryMethod.MakeGenericMethod(selectedType); - var result = typedQuery.Invoke(null, [executor, sql, param, null, CommandType.Text]); + var result = typedQuery.Invoke(null, [ado, sql, param, null, CommandType.Text]); if (include.NavigateInfo!.IsMultiResult) { var tolist = ToList.MakeGenericMethod(selectedType); @@ -73,11 +73,11 @@ public static void Do(ISqlExecutor executor, object item, IncludeInfo include) { foreach (var thenInclude in include.ThenIncludes) { - Do(executor, result, thenInclude); + Do(ado, result, thenInclude); } } } - + #if NET8_0_OR_GREATER [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "这是反射路径")] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "这是反射路径")] @@ -130,20 +130,20 @@ private static SelectBuilder BuildSql(IDatabaseAdapter database, IncludeInfo inc } return selectSql; - + static LambdaExpression BuildSelectAllExpression(ParameterExpression[] allTables) { var lambda = Expression.Lambda(allTables[0], allTables); return lambda; } - + static LambdaExpression BuildMainWhereExpression(object item, ITableColumnInfo col, ParameterExpression[] allTables) { var p = allTables.Last(); var equal = Expression.Equal(Expression.Property(p, col.PropertyName), Expression.Constant(col.GetValue(item))); return Expression.Lambda(equal, allTables); } - + static LambdaExpression BuildIncludeNavigateExpression(SelectBuilder _, Expression body) { if (body.TryGetLambdaExpression(out var mainLambda)) @@ -153,7 +153,7 @@ static LambdaExpression BuildIncludeNavigateExpression(SelectBuilder _, Expressi throw new LightOrmException("Include的Where条件不是一个LambdaExpression"); } - + } } } \ No newline at end of file diff --git a/src/LightORM/Extension/SelectExtensions.cs b/src/LightORM/Extension/SelectExtensions.cs index 0f6c01ee..fca8908d 100644 --- a/src/LightORM/Extension/SelectExtensions.cs +++ b/src/LightORM/Extension/SelectExtensions.cs @@ -51,7 +51,7 @@ public async Task> ToDynamicListAsync(Expression public DataTable ToDataTable(Expression> exp) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTable(sql, parameters); } @@ -59,7 +59,7 @@ public DataTable ToDataTable(Expression> exp) public Task ToDataTableAsync(Expression> exp, CancellationToken cancellationToken = default) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTableAsync(sql, parameters, cancellationToken: cancellationToken); } @@ -82,7 +82,7 @@ public static IExpSelect Select< var key = GetDbKey(typeof(T1), typeof(T2)); if (key != null) { - return new SelectProvider2(instance.SwitchDatabase(key)); + return new SelectProvider2(instance.CreateScoped(key)); } return new SelectProvider2(instance); @@ -125,7 +125,7 @@ public async Task> ToDynamicListAsync(Expression> exp) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTable(sql, parameters); } @@ -133,7 +133,7 @@ public DataTable ToDataTable(Expression> exp) public Task ToDataTableAsync(Expression> exp, CancellationToken cancellationToken = default) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTableAsync(sql, parameters, cancellationToken: cancellationToken); } @@ -172,7 +172,7 @@ public DataTable ToDataTable(Expression, object>> exp) { var flatExp = FlatTypeSet.Default.Flat(exp)!; select.HandleResult(flatExp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTable(sql, parameters); } @@ -181,7 +181,7 @@ public Task ToDataTableAsync(Expression, object> { var flatExp = FlatTypeSet.Default.Flat(exp)!; select.HandleResult(flatExp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTableAsync(sql, parameters, cancellationToken: cancellationToken); } diff --git a/src/LightORM/Extension/SelectResultExtensions.cs b/src/LightORM/Extension/SelectResultExtensions.cs index 8a3bf490..db6490ae 100644 --- a/src/LightORM/Extension/SelectResultExtensions.cs +++ b/src/LightORM/Extension/SelectResultExtensions.cs @@ -15,7 +15,7 @@ public IEnumerable InternalToList< TReturn>() { if (select.IsSubQuery) return []; - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.Execute(sql, parameters).ToList(); } @@ -27,7 +27,7 @@ public TReturn? InternalSingle< TReturn>() { if (select.IsSubQuery) return default; - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.Execute(sql, parameters).Single(); } @@ -38,7 +38,7 @@ public Task> InternalToListAsync< #endif TReturn>(CancellationToken cancellationToken = default) { - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.Execute(sql, parameters).ToListAsync(cancellationToken); } @@ -48,7 +48,7 @@ public IAsyncEnumerable InternalToEnumerableAsync< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TReturn>(CancellationToken cancellationToken = default) { - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.Execute(sql, parameters).ToAsyncList(cancellationToken); } @@ -60,21 +60,21 @@ public Task InternalSingleAsync< #endif TReturn>(CancellationToken cancellationToken = default) { - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.Execute(sql, parameters).SingleAsync(cancellationToken); } public T? ExecuteScalar() { - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteScalar(sql, parameters).As(); } public async Task ExecuteScalarAsync(CancellationToken cancellationToken = default) { - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return (await select.Executor.ExecuteScalarAsync(sql, parameters, cancellationToken: cancellationToken)).As(); } diff --git a/src/LightORM/Interfaces/ExpSql/IExpSelect.cs b/src/LightORM/Interfaces/ExpSql/IExpSelect.cs index 5800b2a5..b88eaf13 100644 --- a/src/LightORM/Interfaces/ExpSql/IExpSelect.cs +++ b/src/LightORM/Interfaces/ExpSql/IExpSelect.cs @@ -4,7 +4,7 @@ public interface IExpSelect : ISql { internal bool IsSubQuery { get; set; } internal IContext DbContext { get; } - internal ISqlExecutor Executor { get; } + internal SqlAdo Executor { get; } internal SelectBuilder SqlBuilder { get; } } diff --git a/src/LightORM/Interfaces/IExpressionContext.cs b/src/LightORM/Interfaces/IExpressionContext.cs index e8dcba65..2df6f64c 100644 --- a/src/LightORM/Interfaces/IExpressionContext.cs +++ b/src/LightORM/Interfaces/IExpressionContext.cs @@ -5,7 +5,7 @@ namespace LightORM; public interface IContext { - ISqlExecutor Ado { get; } + SqlAdo Ado { get; } MultipleResult QueryMultiple(params IExpSelect[] selects); Task QueryMultipleAsync(IExpSelect[] selects, CancellationToken cancellationToken); @@ -37,7 +37,7 @@ public interface IExpressionContext : IDisposable, ITableAction, IContext { string Id { get; } internal ExpressionSqlOptions Options { get; } - internal ISqlExecutor GetAdo(string key); + internal SqlAdo GetAdo(string key); /// /// 与不同的是,当Union个数大于1时,该方法会嵌套为子查询 /// @@ -79,7 +79,7 @@ IExpSelect FromTemp< //IExpDelete Delete(); //IExpDelete Delete(params T[] entities); ISingleScopedExpressionContext Use(IDatabaseProvider db); - ITransientExpressionContext SwitchDatabase(string key); + TransientExpressionContext SwitchDatabase(string key); /// /// 创建指定数据库的单元操作对象,支持事务 @@ -124,31 +124,23 @@ public interface IDefinedTableAction /// public interface IScopedExpressionContext : IDisposable, IDefinedTableAction, IContext { - ISqlExecutor DefaultAdo { get; } - ITransientExpressionContext SwitchDatabase(string key); + SqlAdo DefaultAdo { get; } + TransientExpressionContext SwitchDatabase(string key); string Id { get; } -// IExpSelect Select< -//#if NET8_0_OR_GREATER -// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] -//#endif -// T>(); -// IExpInsert Insert(params T[] entities); -// IExpUpdate Update(); -// IExpUpdate Update(params T[] entities); -// IExpDelete Delete(); -// IExpDelete Delete(params T[] entity); void BeginTransaction(string key = ConstString.Main, IsolationLevel isolationLevel = IsolationLevel.Unspecified); - Task BeginTransactionAsync(string key = ConstString.Main, IsolationLevel isolationLevel = IsolationLevel.Unspecified); void CommitTransaction(string key = ConstString.Main); - Task CommitTransactionAsync(string key = ConstString.Main); void RollbackTransaction(string key = ConstString.Main); - Task RollbackTransactionAsync(string key = ConstString.Main); void BeginAllTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified); - Task BeginAllTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified); void CommitAllTransaction(); - Task CommitAllTransactionAsync(); void RollbackAllTransaction(); - Task RollbackAllTransactionAsync(); +#if NET8_0_OR_GREATER + Task BeginTransactionAsync(string key = ConstString.Main, IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default); + Task CommitTransactionAsync(string key = ConstString.Main, CancellationToken cancellationToken = default); + Task RollbackTransactionAsync(string key = ConstString.Main, CancellationToken cancellationToken = default); + Task BeginAllTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default); + Task CommitAllTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackAllTransactionAsync(CancellationToken cancellationToken = default); +#endif } /// @@ -157,22 +149,14 @@ public interface IScopedExpressionContext : IDisposable, IDefinedTableAction, IC public interface ISingleScopedExpressionContext : IDisposable, IDefinedTableAction, IContext { string Id { get; } -// IExpSelect Select< -//#if NET8_0_OR_GREATER -// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] -//#endif -// T>(); -// IExpInsert Insert(params T[] entities); -// IExpUpdate Update(); -// IExpUpdate Update(params T[] entities); -// IExpDelete Delete(); -// IExpDelete Delete(params T[] entity); void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified); - Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified); void CommitTransaction(); - Task CommitTransactionAsync(); void RollbackTransaction(); - Task RollbackTransactionAsync(); +#if NET8_0_OR_GREATER + Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default); + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); +#endif internal void TryBeginTransaction(); internal void TryCommitTransaction(); internal void TryRollbackTransaction(); diff --git a/src/LightORM/Interfaces/ISqlExecutor.cs b/src/LightORM/Interfaces/ISqlExecutor.cs index f00b6da9..09288ee7 100644 --- a/src/LightORM/Interfaces/ISqlExecutor.cs +++ b/src/LightORM/Interfaces/ISqlExecutor.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using LightORM.Performances; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -8,7 +9,7 @@ public partial interface ISqlExecutor { internal ConnectionPool Pool { get; } internal AdoInterceptor Interceptor { get; } - internal IDatabaseProvider Database { get; } + internal IDatabaseProvider Provider { get; } internal void InitTransactionContext(); internal void InitTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified); /// @@ -19,6 +20,7 @@ public partial interface ISqlExecutor /// internal Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default); } +[Obsolete("", true)] public partial interface ISqlExecutor : IDisposable, ICloneable { ///// @@ -75,7 +77,7 @@ int ExecuteNonQuery< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] #endif - TParameter>(string commandText, TParameter dbParameters , CommandType commandType = CommandType.Text); + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text); /// /// 执行标量 @@ -88,7 +90,7 @@ ScalarValue ExecuteScalar< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] #endif - TParameter>(string commandText, TParameter dbParameters , CommandType commandType = CommandType.Text); + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text); /// /// 执行阅读器 diff --git a/src/LightORM/Models/AdoInterceptor.cs b/src/LightORM/Models/AdoInterceptor.cs index 6941f9f3..8aa5b0f9 100644 --- a/src/LightORM/Models/AdoInterceptor.cs +++ b/src/LightORM/Models/AdoInterceptor.cs @@ -25,12 +25,7 @@ public static void NotifyException(this AdoInterceptor interceptor, SqlExecuteEx interceptor.Interceptors.ForEach(t => t.OnException(context)); } } -internal readonly struct AdoInterceptor +internal readonly struct AdoInterceptor(ICollection interceptors) { - public ICollection Interceptors { get; } - - public AdoInterceptor(IEnumerable interceptors) - { - Interceptors = [.. interceptors]; - } + public ICollection Interceptors { get; } = interceptors; } diff --git a/src/LightORM/Models/ScalarValue.cs b/src/LightORM/Models/ScalarValue.cs index a1116904..148a766a 100644 --- a/src/LightORM/Models/ScalarValue.cs +++ b/src/LightORM/Models/ScalarValue.cs @@ -10,13 +10,14 @@ public static class ScalarValueExtension { return default; } - return SqlExecutor.SqlExecutor.ChangeType(scalarValue.Value); + return SqlExecutor.SqlAdo.ChangeType(scalarValue.Value); } } } -public readonly record struct ScalarValue(object? Value) +public readonly struct ScalarValue(object? value) { + public object? Value { get; } = value; /// /// 是否为 DBNull 或 null /// @@ -27,6 +28,7 @@ public readonly record struct ScalarValue(object? Value) /// public bool HasValue => !IsNull; + // string public static implicit operator string?(ScalarValue scalarValue) => scalarValue.As(); // int and int? diff --git a/src/LightORM/SqlExecutor/ConnectionPool.cs b/src/LightORM/Performances/ConnectionPool.cs similarity index 91% rename from src/LightORM/SqlExecutor/ConnectionPool.cs rename to src/LightORM/Performances/ConnectionPool.cs index 8d0cc4c8..2da9a078 100644 --- a/src/LightORM/SqlExecutor/ConnectionPool.cs +++ b/src/LightORM/Performances/ConnectionPool.cs @@ -1,8 +1,11 @@ -using System.Data.Common; +using System.Collections.Concurrent; +using System.Data.Common; using System.Diagnostics; -namespace LightORM.SqlExecutor; +namespace LightORM.Performances; internal class ConnectionPool(Func func, int maxCapacity) : ObjectPool(func,maxCapacity) { + internal static readonly ConcurrentDictionary Pools = []; + private readonly TimeSpan _connectionLifetime = TimeSpan.FromMinutes(15); protected override void HandleOverflowObject(DbConnection item) diff --git a/src/LightORM/SqlExecutor/ObjectPool.cs b/src/LightORM/Performances/ObjectPool.cs similarity index 99% rename from src/LightORM/SqlExecutor/ObjectPool.cs rename to src/LightORM/Performances/ObjectPool.cs index 60378ce2..99d6f271 100644 --- a/src/LightORM/SqlExecutor/ObjectPool.cs +++ b/src/LightORM/Performances/ObjectPool.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Collections.Concurrent; using System.Diagnostics; -namespace LightORM.SqlExecutor; +namespace LightORM.Performances; internal abstract class ObjectPool : IDisposable where T : class, IDisposable diff --git a/src/LightORM/Providers/DeleteProvider.cs b/src/LightORM/Providers/DeleteProvider.cs index b858a7ae..b7613f7c 100644 --- a/src/LightORM/Providers/DeleteProvider.cs +++ b/src/LightORM/Providers/DeleteProvider.cs @@ -6,22 +6,22 @@ namespace LightORM.Providers { internal sealed class DeleteProvider : IExpDelete { - private readonly ISqlExecutor executor; + private readonly SqlAdo ado; private readonly DeleteBuilder sqlBuilder; - private IDatabaseAdapter Database => executor.Database.DatabaseAdapter; + private IDatabaseAdapter Database => ado.Provider.DatabaseAdapter; //public bool ForceDelete { get => sqlBuilder.ForceDelete; set => sqlBuilder.ForceDelete = value; } //public bool Truncate { get => sqlBuilder.Truncate; set => sqlBuilder.Truncate = value; } - public DeleteProvider(ISqlExecutor executor, T? entity) + public DeleteProvider(SqlAdo executor, T? entity) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.TargetObject = entity; } - public DeleteProvider(ISqlExecutor executor, T[] entities) + public DeleteProvider(SqlAdo executor, T[] entities) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.TargetObjects = entities; @@ -49,8 +49,34 @@ public IExpDelete QuoteIdentifiers() public int Execute() { var sql = sqlBuilder.ToSqlString(Database); - var dbParameters = sqlBuilder.DbParameters; - return executor.ExecuteNonQuery(sql, dbParameters); + if (sqlBuilder.IsBatchDelete) + { + try + { + var effectRows = 0; + if (ado.Connection.UnderTransaction) + ado.Connection.BeginTransaction(); + foreach (var item in sqlBuilder.BatchInfos!) + { + effectRows += ado.ExecuteNonQuery(item.Sql!, item.ToDictionaryParameters()); + } + if (ado.Connection.UnderTransaction) + ado.Connection.CommitTransaction(); + return effectRows; + } + catch + { + if (ado.Connection.UnderTransaction) + ado.Connection.RollbackTransaction(); + throw; + } + + } + else + { + var dbParameters = sqlBuilder.DbParameters; + return ado.ExecuteNonQuery(sql, dbParameters); + } } public async Task ExecuteAsync(CancellationToken cancellationToken = default) @@ -61,29 +87,40 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = defaul var sql = sqlBuilder.ToSqlString(Database); if (sqlBuilder.IsBatchDelete) { - var usingTransaction = executor.DbTransaction == null; try { var effectRows = 0; - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - executor.BeginTransaction(); +#if NET8_0_OR_GREATER + await ado.Connection.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); +#else + ado.Connection.BeginTransaction(); +#endif } foreach (var item in sqlBuilder.BatchInfos!) { - effectRows += await executor.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); + effectRows += await ado.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); } - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - await executor.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await ado.Connection.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.CommitTransaction(); +#endif } return effectRows; } catch { - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - await executor.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await ado.Connection.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.RollbackTransaction(); +#endif } throw; } @@ -92,7 +129,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = defaul else { var dbParameters = sqlBuilder.DbParameters; - return await executor.ExecuteNonQueryAsync(sql, dbParameters, cancellationToken: cancellationToken); + return await ado.ExecuteNonQueryAsync(sql, dbParameters, cancellationToken: cancellationToken); } } diff --git a/src/LightORM/Providers/GroupSelectProvider.cs b/src/LightORM/Providers/GroupSelectProvider.cs index c03bb4cd..577d04df 100644 --- a/src/LightORM/Providers/GroupSelectProvider.cs +++ b/src/LightORM/Providers/GroupSelectProvider.cs @@ -9,8 +9,8 @@ internal class GroupSelectProvider : IExpSelectGroup DbContext.Ado; - public IDatabaseAdapter Database => Executor.Database.DatabaseAdapter; + public SqlAdo Executor => DbContext.Ado; + public IDatabaseAdapter Database => Executor.Provider.DatabaseAdapter; public bool IsSubQuery { get; set; } public GroupSelectProvider(IContext dbContext, SelectBuilder builder, LambdaExpression keySelector) { diff --git a/src/LightORM/Providers/InsertProvider.cs b/src/LightORM/Providers/InsertProvider.cs index 0e713177..69068751 100644 --- a/src/LightORM/Providers/InsertProvider.cs +++ b/src/LightORM/Providers/InsertProvider.cs @@ -6,20 +6,20 @@ namespace LightORM.Providers; internal sealed class InsertProvider : IExpInsert { - private readonly ISqlExecutor executor; + private readonly SqlAdo ado; private readonly InsertBuilder sqlBuilder; - private IDatabaseAdapter Database => executor.Database.DatabaseAdapter; - public InsertProvider(ISqlExecutor executor, T? entity) + private IDatabaseAdapter Database => ado.Provider.DatabaseAdapter; + public InsertProvider(SqlAdo executor, T? entity) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.TargetObject = entity; } - public InsertProvider(ISqlExecutor executor, T[] entities) + public InsertProvider(SqlAdo executor, T[] entities) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.TargetObjects = entities; @@ -146,24 +146,23 @@ public int Execute() var sql = sqlBuilder.ToSqlString(Database); if (sqlBuilder.IsBatchInsert) { - var usingTransaction = executor.DbTransaction != null; try { var effectRows = 0; - if (usingTransaction) - executor.BeginTransaction(); + if (ado.Connection.UnderTransaction) + ado.Connection.BeginTransaction(); foreach (var item in sqlBuilder.BatchInfos!) { - effectRows += executor.ExecuteNonQuery(item.Sql!, item.ToDictionaryParameters()); + effectRows += ado.ExecuteNonQuery(item.Sql!, item.ToDictionaryParameters()); } - if (usingTransaction) - executor.CommitTransaction(); + if (ado.Connection.UnderTransaction) + ado.Connection.CommitTransaction(); return effectRows; } catch { - if (usingTransaction) - executor.RollbackTransaction(); + if (ado.Connection.UnderTransaction) + ado.Connection.RollbackTransaction(); throw; } @@ -171,7 +170,7 @@ public int Execute() else { var dbParameters = sqlBuilder.DbParameters; - return executor.ExecuteNonQuery(sql, dbParameters); + return ado.ExecuteNonQuery(sql, dbParameters); } } @@ -180,31 +179,48 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = defaul var sql = sqlBuilder.ToSqlString(Database); if (sqlBuilder.IsBatchInsert) { - var usingTransaction = executor.DbTransaction != null; try { var effectRows = 0; - if (usingTransaction) - executor.BeginTransaction(); + if (ado.Connection.UnderTransaction) + { +#if NET8_0_OR_GREATER + await ado.Connection.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); +#else + ado.Connection.BeginTransaction(); +#endif + } foreach (var item in sqlBuilder.BatchInfos!) { - effectRows += await executor.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); + effectRows += await ado.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); + } + if (ado.Connection.UnderTransaction) + { +#if NET8_0_OR_GREATER + await ado.Connection.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.CommitTransaction(); +#endif } - if (usingTransaction) - await executor.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); return effectRows; } catch { - if (usingTransaction) - await executor.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); + if (ado.Connection.UnderTransaction) + { +#if NET8_0_OR_GREATER + await ado.Connection.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.RollbackTransaction(); +#endif + } throw; } } else { var parameters = sqlBuilder.DbParameters; - return await executor.ExecuteNonQueryAsync(sql, parameters, cancellationToken: cancellationToken).ConfigureAwait(false); + return await ado.ExecuteNonQueryAsync(sql, parameters, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/src/LightORM/Providers/Select/SelectInsertProvider.cs b/src/LightORM/Providers/Select/SelectInsertProvider.cs index a0434a7f..479a4edd 100644 --- a/src/LightORM/Providers/Select/SelectInsertProvider.cs +++ b/src/LightORM/Providers/Select/SelectInsertProvider.cs @@ -4,10 +4,10 @@ namespace LightORM.Providers.Select { internal class SelectInsertProvider : ISelectInsert { - private readonly ISqlExecutor executor; + private readonly SqlAdo executor; private readonly SelectBuilder builder; - private IDatabaseAdapter Database => executor.Database.DatabaseAdapter; - public SelectInsertProvider(ISqlExecutor executor, SelectBuilder builder) + private IDatabaseAdapter Database => executor.Provider.DatabaseAdapter; + public SelectInsertProvider(SqlAdo executor, SelectBuilder builder) { this.executor = executor; this.builder = builder; diff --git a/src/LightORM/Providers/Select/SelectProvider0.cs b/src/LightORM/Providers/Select/SelectProvider0.cs index 7f6fb1af..a7eae4c2 100644 --- a/src/LightORM/Providers/Select/SelectProvider0.cs +++ b/src/LightORM/Providers/Select/SelectProvider0.cs @@ -15,9 +15,9 @@ internal class SelectProvider0 : IExpSelect0 where TSelect : class, IExpSelect { public SelectBuilder SqlBuilder { get; set; } = default!; - public ISqlExecutor Executor => DbContext.Ado; - public DbBaseType DbType => Executor.Database.DbBaseType; - public IDatabaseAdapter Database => Executor.Database.DatabaseAdapter; + public SqlAdo Executor => DbContext.Ado; + public DbBaseType DbType => Executor.Provider.DbBaseType; + public IDatabaseAdapter Database => Executor.Provider.DatabaseAdapter; public bool IsSubQuery { get; set; } public IContext DbContext { get; } diff --git a/src/LightORM/Providers/Select/SelectProvider1.cs b/src/LightORM/Providers/Select/SelectProvider1.cs index ee25f6c5..ba01bc47 100644 --- a/src/LightORM/Providers/Select/SelectProvider1.cs +++ b/src/LightORM/Providers/Select/SelectProvider1.cs @@ -491,7 +491,7 @@ public ISelectInsert Insert() public ISelectInsert Insert(Expression> exp) { var table = TableContext.GetTableInfo(); - var result = exp.Resolve(SqlResolveOptions.Insert, ResolveContext.Create(Executor.Database.DbBaseType)); + var result = exp.Resolve(SqlResolveOptions.Insert, ResolveContext.Create(Executor.Provider.DbBaseType)); HandleSelectInsert(table.TableName, result.SqlString!); //var sql = SqlBuilder.ToSqlString(); SqlBuilder.Expressions.Update(e => e.ResolveOptions == SqlResolveOptions.Select, e => diff --git a/src/LightORM/Providers/UpdateProvider.cs b/src/LightORM/Providers/UpdateProvider.cs index 1c19d9e0..a604d2ad 100644 --- a/src/LightORM/Providers/UpdateProvider.cs +++ b/src/LightORM/Providers/UpdateProvider.cs @@ -6,20 +6,20 @@ namespace LightORM.Providers { internal class UpdateProvider : IExpUpdate { - private readonly ISqlExecutor executor; + private readonly SqlAdo ado; private readonly UpdateBuilder sqlBuilder; - private IDatabaseAdapter Database => executor.Database.DatabaseAdapter; - public UpdateProvider(ISqlExecutor executor, T? entity) + private IDatabaseAdapter Database => ado.Provider.DatabaseAdapter; + public UpdateProvider(SqlAdo executor, T? entity) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.TargetObject = entity; } - public UpdateProvider(ISqlExecutor executor, T[] entities) + public UpdateProvider(SqlAdo executor, T[] entities) { - this.executor = executor; + this.ado = executor; sqlBuilder = new(); sqlBuilder.SelectedTables.Add(TableInfo.Create()); sqlBuilder.IsBatchUpdate = true; @@ -48,29 +48,28 @@ public int Execute() var sql = sqlBuilder.ToSqlString(Database); if (sqlBuilder.IsBatchUpdate) { - var usingTransaction = executor.DbTransaction != null; try { var effectRows = 0; - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - executor.BeginTransaction(); + ado.Connection.BeginTransaction(); } foreach (var item in sqlBuilder.BatchInfos!) { - effectRows += executor.ExecuteNonQuery(item.Sql!, item.ToDictionaryParameters()); + effectRows += ado.ExecuteNonQuery(item.Sql!, item.ToDictionaryParameters()); } - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - executor.CommitTransaction(); + ado.Connection.CommitTransaction(); } return effectRows; } catch { - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - executor.RollbackTransaction(); + ado.Connection.RollbackTransaction(); } throw; } @@ -79,7 +78,7 @@ public int Execute() else { var dbParameters = sqlBuilder.DbParameters; - return executor.ExecuteNonQuery(sql, dbParameters); + return ado.ExecuteNonQuery(sql, dbParameters); } } @@ -88,29 +87,40 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = defaul var sql = sqlBuilder.ToSqlString(Database); if (sqlBuilder.IsBatchUpdate) { - var usingTransaction = executor.DbTransaction == null; try { var effectRows = 0; - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - executor.BeginTransaction(); +#if NET8_0_OR_GREATER + await ado.Connection.BeginTransactionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); +#else + ado.Connection.BeginTransaction(); +#endif } foreach (var item in sqlBuilder.BatchInfos!) { - effectRows += await executor.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); + effectRows += await ado.ExecuteNonQueryAsync(item.Sql!, item.ToDictionaryParameters(), cancellationToken: cancellationToken).ConfigureAwait(false); } - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - await executor.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await ado.Connection.CommitTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.CommitTransaction(); +#endif } return effectRows; } catch { - if (usingTransaction) + if (ado.Connection.UnderTransaction) { - await executor.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await ado.Connection.RollbackTransactionAsync(cancellationToken).ConfigureAwait(false); +#else + ado.Connection.RollbackTransaction(); +#endif } throw; } @@ -119,7 +129,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = defaul else { var dbParameters = sqlBuilder.DbParameters; - return await executor.ExecuteNonQueryAsync(sql, dbParameters, cancellationToken: cancellationToken); + return await ado.ExecuteNonQueryAsync(sql, dbParameters, cancellationToken: cancellationToken); } } public IExpUpdate SetNullIf(bool condition, Expression> exp) diff --git a/src/LightORM/Repository/LightOrmQueryProvider.cs b/src/LightORM/Repository/LightOrmQueryProvider.cs index a0334235..fa1422dd 100644 --- a/src/LightORM/Repository/LightOrmQueryProvider.cs +++ b/src/LightORM/Repository/LightOrmQueryProvider.cs @@ -9,9 +9,9 @@ namespace LightORM.Repository; internal class LightOrmQueryProvider : IQueryProvider { private readonly SelectBuilder select = SelectBuilder.GetSelectBuilder(); - private readonly ISqlExecutor ado; + private readonly SqlAdo ado; private LambdaExpression? keySelector; - public LightOrmQueryProvider(ISqlExecutor ado, Type type) + public LightOrmQueryProvider(SqlAdo ado, Type type) { this.ado = ado; select.AddTableInfo(TableInfo.Create(type)); @@ -145,13 +145,13 @@ private static LightOrmQuery ReturnOrCreateQuery(MethodCallE public object Execute(Expression expression) { - var sql = select.ToSqlString(ado.Database.DatabaseAdapter); + var sql = select.ToSqlString(ado.Provider.DatabaseAdapter); return ado.ExecuteReader(sql, select.DbParameters); } public TResult Execute(Expression expression) { - var sql = select.ToSqlString(ado.Database.DatabaseAdapter); + var sql = select.ToSqlString(ado.Provider.DatabaseAdapter); var def = ado.Execute(sql, select.DbParameters).ToList().FirstOrDefault(); if (def == null && expression is MethodCallExpression method && method.Arguments.Count > 1) { diff --git a/src/LightORM/SqlExecutor/DatabaseConnection.cs b/src/LightORM/SqlExecutor/DatabaseConnection.cs new file mode 100644 index 00000000..378ae971 --- /dev/null +++ b/src/LightORM/SqlExecutor/DatabaseConnection.cs @@ -0,0 +1,339 @@ +using LightORM.Performances; +using System.Data.Common; +using System.Diagnostics; +using System.Threading; +namespace LightORM.SqlExecutor; + +internal enum AdoState +{ + Active, + Committed, + Rollback, + OccurException +} +public class DatabaseConnection : IDisposable +{ + private bool disposed; + public bool IsOccurException => State == AdoState.OccurException; + internal AdoState State { get; private set; } + public IDatabaseProvider Provider { get; } + internal AdoInterceptor Interceptor { get; } + public DbConnection Connection { get; set; } + public DbTransaction? Transaction { get; set; } + public DbConnection GetCurrentConnection() => Transaction?.Connection ?? Connection; + public bool UnderTransaction => Transaction is not null; + public int TransactionNestLevel { get; set; } + public int Id => Connection.GetHashCode(); + public bool IsExternal { get; set; } + + internal DatabaseConnection(DbConnection connection + , IDatabaseProvider provider + , AdoInterceptor adoInterceptor + , DbTransaction? transaction = null) + { + Provider = provider; + Interceptor = adoInterceptor; + Connection = transaction?.Connection ?? connection; + Transaction = transaction; + } + + public void UseExternalTransaction(DbTransaction dbTransaction) + { + if (dbTransaction.Connection is null) + throw new InvalidOperationException("External transaction must have a valid connection"); + IsExternal = true; + Transaction = dbTransaction; + Connection = dbTransaction.Connection; + } + + public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + { + try + { + ObjectDisposedException.ThrowIf(disposed, this); + if (Transaction is null) + { + if (Connection.State != ConnectionState.Open) + { + Connection.Open(); + } + Transaction = isolationLevel == IsolationLevel.Unspecified + ? Connection.BeginTransaction() + : Connection.BeginTransaction(isolationLevel); + } + else + { + TransactionNestLevel++; +#if NET6_0_OR_GREATER + if (Transaction.SupportsSavepoints) + { + Transaction.Save($"savePoint{TransactionNestLevel}"); + } +#endif + } + } + catch (Exception ex) + { + State = AdoState.OccurException; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.BeginTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw; + } + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"BeginTran: {Id} -> {TransactionNestLevel}"); + } + + public void CommitTransaction() + { + if (Transaction is null) + { + if (IsOccurException == true) + { + // 如果BeginTransaction发生的异常没有处理,不会进入到CommitTransaction,如果运行到这里,说明异常已经处理了,直接return + return; + } + var ex = new InvalidOperationException("No active transaction to commit"); ; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.CommitTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw ex; + } + if (TransactionNestLevel > 0) + { + // 嵌套事务只减少计数器 + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"CommitTran: {Id} -> {TransactionNestLevel}"); + TransactionNestLevel--; + return; + } + // 最外层事务提交 + try + { + Transaction.Commit(); + State = AdoState.Committed; + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"CommitTran: {Id} -> finished"); + } + catch (Exception ex) + { + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.CommitTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + RollbackTransaction(); + State = AdoState.OccurException; + } + finally + { + Dispose(); + } + } + + public void RollbackTransaction() + { + if (Transaction is null) + { + if (IsOccurException == true) + { + // 如果BeginTransaction发生的异常没有处理,不会进入到CommitTransaction,如果运行到这里,说明异常已经处理了,直接return + return; + } + var ex = new InvalidOperationException("No active transaction to rollback"); ; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.RollbackTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw ex; + } + if (TransactionNestLevel > 0) + { +#if NET6_0_OR_GREATER + if (Transaction.SupportsSavepoints) + { + Transaction.Rollback($"savePoint{TransactionNestLevel}"); + } +#endif + TransactionNestLevel--; + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"RollbackTran: {Id} -> {TransactionNestLevel}"); + return; + } + try + { + Transaction.Rollback(); + State = AdoState.Rollback; + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"RollbackTran: {Id} -> finished"); + } + catch (Exception ex) + { + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.RollbackTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + } + finally + { + Dispose(); + } + } + + #region 异步API + +#if NET6_0_OR_GREATER + public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default) + { + try + { + ObjectDisposedException.ThrowIf(disposed, this); + if (Transaction is null) + { + if (Connection.State != ConnectionState.Open) + { + await Connection.OpenAsync(cancellationToken).ConfigureAwait(false); + } + Transaction = isolationLevel == IsolationLevel.Unspecified + ? await Connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false) + : await Connection.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false); + } + else + { + // 嵌套事务 + TransactionNestLevel++; + if (Transaction.SupportsSavepoints) + { + Transaction.Save($"savePoint{TransactionNestLevel}"); + } + } + } + catch (Exception ex) + { + State = AdoState.OccurException; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.BeginTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw; + } + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"BeginTranAsync: {Id} -> {TransactionNestLevel}"); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + if (Transaction is null) + { + if (IsOccurException == true) + { + // 如果BeginTransaction发生的异常没有处理,不会进入到CommitTransaction,如果运行到这里,说明异常已经处理了,直接return + return; + } + var ex = new InvalidOperationException("No active transaction to commit"); ; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.CommitTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw ex; + } + if (TransactionNestLevel > 0) + { + // 嵌套事务只减少计数器 + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"CommitTranAsync: {Id} -> {TransactionNestLevel}"); + TransactionNestLevel--; + return; + } + + // 最外层事务提交 + try + { + await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + State = AdoState.Committed; + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"CommitTranAsync: {Id} -> finished"); + } + catch (Exception ex) + { + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.CommitTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + await RollbackTransactionAsync(cancellationToken); + State = AdoState.OccurException; + } + finally + { + Dispose(); + } + } + + public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + if (Transaction is null) + { + if (IsOccurException == true) + { + // 如果发生的异常没有处理,不会进入到这里,如果运行到这里,说明异常已经处理了,直接return + return; + } + var ex = new InvalidOperationException("No active transaction to rollback"); ; + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.RollbackTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + if (ctx.IsHandled) + { + return; + } + throw ex; + } + if (TransactionNestLevel > 0) + { + if (Transaction.SupportsSavepoints) + { + await Transaction.RollbackAsync($"savePoint{TransactionNestLevel}", cancellationToken).ConfigureAwait(false); + } + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"RollbackTranAsync: {Id} -> {TransactionNestLevel}"); + TransactionNestLevel--; + return; + } + try + { + await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); + State = AdoState.Rollback; + Debug.WriteLineIf(ShowSqlExecutorDebugInfo, $"RollbackTranAsync: {Id} -> finished"); + } + catch(Exception ex) + { + var ctx = new SqlExecuteExceptionContext(new SqlExecuteContext(ExecuteMethod.RollbackTransaction, null, null, typeof(object)), ex); + Interceptor.NotifyException(ctx); + } + finally + { + Dispose(); + } + } +#endif + + #endregion + + public void Dispose() + { + if (disposed) + return; + // 内部事务创建的事务上下文 + if (!IsExternal) + { + if (Connection is not null) + { + if (Connection.State != ConnectionState.Closed) + { + Connection.Close(); + } + var pool = ConnectionPool.Pools[Provider]; + pool.Return(Connection); + } + Transaction?.Dispose(); + Transaction = null; + } + disposed = true; + GC.SuppressFinalize(this); + } +} diff --git a/src/LightORM/SqlExecutor/ExecuteResult.cs b/src/LightORM/SqlExecutor/ExecuteResult.cs index 57b434d7..0375f5e1 100644 --- a/src/LightORM/SqlExecutor/ExecuteResult.cs +++ b/src/LightORM/SqlExecutor/ExecuteResult.cs @@ -9,9 +9,9 @@ public readonly struct ExecuteResult< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] #endif -TParameter>(ISqlExecutor ado, string sql, TParameter? param, DbTransaction? trans, CommandType commandType = CommandType.Text) +TParameter>(SqlAdo ado, string sql, TParameter? param, DbTransaction? trans, CommandType commandType = CommandType.Text) { - internal ISqlExecutor Ado { get; } = ado; + internal SqlAdo Ado { get; } = ado; internal string Sql { get; } = sql; internal TParameter? Param { get; } = param; internal DbTransaction? Trans { get; } = trans; @@ -197,7 +197,6 @@ public async IAsyncEnumerable ToAsyncList< { await reader.CloseAsync(); } - reader?.Close(); } } @@ -227,7 +226,6 @@ public async IAsyncEnumerable ToAsyncList([EnumeratorCancellation] Canc { await reader.CloseAsync(); } - reader?.Close(); } } #endif @@ -269,9 +267,9 @@ public async Task SingleAsync< } -public readonly struct ExecuteResult(ISqlExecutor ado, string sql, DbTransaction? trans, CommandType commandType = CommandType.Text) +public readonly struct ExecuteResult(SqlAdo ado, string sql, DbTransaction? trans, CommandType commandType = CommandType.Text) { - internal ISqlExecutor Ado { get; } = ado; + internal SqlAdo Ado { get; } = ado; internal string Sql { get; } = sql; internal NullDbParameter Param { get; } = NullDbParameter.Instance; internal DbTransaction? Trans { get; } = trans; diff --git a/src/LightORM/SqlExecutor/ExpressionBuilder.cs b/src/LightORM/SqlExecutor/ExpressionBuilder.cs index cdcda13d..09a1bc31 100644 --- a/src/LightORM/SqlExecutor/ExpressionBuilder.cs +++ b/src/LightORM/SqlExecutor/ExpressionBuilder.cs @@ -151,12 +151,7 @@ Expression CreateAnonymous( foreach (var targetMember in props) { Expression TargetValueExpression; - var ordinal = reader.GetOrdinal(targetMember.Name); - if (ContainsJsonType(targetMember.PropertyType)) - { - TargetValueExpression = GetTargetJsonExpression(reader, Culture, recordInstanceExp, SchemaTable, ordinal, targetMember.PropertyType); - } - else if (targetMember.PropertyType.IsClassOrAnonymous()) + if (targetMember.PropertyType.IsClassOrAnonymous()) { if (targetMember.PropertyType.IsAnonymous()) { @@ -171,8 +166,14 @@ Expression CreateAnonymous( TargetValueExpression = CreateCustomEntiry(targetMember.PropertyType); } } + else if (ContainsJsonType(targetMember.PropertyType)) + { + var ordinal = reader.GetOrdinal(targetMember.Name); + TargetValueExpression = GetTargetJsonExpression(reader, Culture, recordInstanceExp, SchemaTable, ordinal, targetMember.PropertyType); + } else { + var ordinal = reader.GetOrdinal(targetMember.Name); TargetValueExpression = GetTargetValueExpression(reader, Culture, recordInstanceExp, SchemaTable, ordinal, targetMember.PropertyType); } diff --git a/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.AOT.cs b/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.AOT.cs index e949ecac..746ae95a 100644 --- a/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.AOT.cs +++ b/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.AOT.cs @@ -6,7 +6,7 @@ namespace LightORM; public static partial class SqlExecutorExtensions { - extension(ISqlExecutor executor) + extension(SqlAdo executor) { public ExecuteResult Execute< #if NET8_0_OR_GREATER diff --git a/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.cs b/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.cs index b9993f59..35095587 100644 --- a/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.cs +++ b/src/LightORM/SqlExecutor/Extensions/SqlExecutorExtensions.cs @@ -11,7 +11,7 @@ public static IEnumerable Query< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>(this ISqlExecutor self + T>(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -41,7 +41,7 @@ public static IEnumerable Query< } } - public static IEnumerable Query(this ISqlExecutor self + public static IEnumerable Query(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -75,7 +75,7 @@ public static T? QuerySingle< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>(this ISqlExecutor self + T>(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -111,7 +111,7 @@ public static async Task> QueryListAsync< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>(this ISqlExecutor self + T>(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -155,7 +155,7 @@ public static async Task> QueryListAsync< } } - public static async Task> QueryListAsync(this ISqlExecutor self + public static async Task> QueryListAsync(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -202,7 +202,7 @@ public static async Task> QueryListAsync(this ISqlExecutor self #if NET8_0_OR_GREATER public static async IAsyncEnumerable QueryAsync< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] - T>(this ISqlExecutor self + T>(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -241,7 +241,7 @@ public static async IAsyncEnumerable QueryAsync< } } - public static async IAsyncEnumerable QueryAsync(this ISqlExecutor self + public static async IAsyncEnumerable QueryAsync(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null @@ -285,7 +285,7 @@ public static async Task QuerySingleAsync< #if NET8_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] #endif - T>(this ISqlExecutor self + T>(this SqlAdo self , string sql , object? param = null , DbTransaction? trans = null diff --git a/src/LightORM/SqlExecutor/InternalDataReaderLight.cs b/src/LightORM/SqlExecutor/InternalDataReaderLight.cs new file mode 100644 index 00000000..6e6178d8 --- /dev/null +++ b/src/LightORM/SqlExecutor/InternalDataReaderLight.cs @@ -0,0 +1,111 @@ +using System.Collections; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace LightORM.SqlExecutor; + +internal class InternalDataReaderLight(DbDataReader dataReader, PrepareResult prepareResult, DatabaseConnection connection) : DbDataReader +{ + public override void Close() + { + dataReader.Close(); + if (!connection.UnderTransaction) + { + connection.Dispose(); + } + prepareResult.Command.Parameters.Clear(); + prepareResult.Command.Dispose(); + } + +#if NET6_0_OR_GREATER + public override async Task CloseAsync() + { + await dataReader.CloseAsync(); + if (!connection.UnderTransaction) + { + connection.Dispose(); + } + prepareResult.Command.Parameters.Clear(); + await prepareResult.Command.DisposeAsync(); + } +#endif + + public override object this[int ordinal] => dataReader[ordinal]; + + public override object this[string name] => dataReader[name]; + + public override int Depth => dataReader.Depth; + + public override int FieldCount => dataReader.FieldCount; + + public override bool HasRows => dataReader.HasRows; + + public override bool IsClosed => dataReader.IsClosed; + + public override int RecordsAffected => dataReader.RecordsAffected; + + public override bool GetBoolean(int ordinal) => dataReader.GetBoolean(ordinal); + + public override byte GetByte(int ordinal) => dataReader.GetByte(ordinal); + + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) + => dataReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); + + public override char GetChar(int ordinal) => dataReader.GetChar(ordinal); + + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) + => dataReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); + + public override string GetDataTypeName(int ordinal) => dataReader.GetDataTypeName(ordinal); + + public override DateTime GetDateTime(int ordinal) => dataReader.GetDateTime(ordinal); + + public override decimal GetDecimal(int ordinal) => dataReader.GetDecimal(ordinal); + + public override double GetDouble(int ordinal) => dataReader.GetDouble(ordinal); + + public override IEnumerator GetEnumerator() => dataReader.GetEnumerator(); +#if NET8_0_OR_GREATER + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + public override Type GetFieldType(int ordinal) => dataReader.GetFieldType(ordinal); + + public override float GetFloat(int ordinal) => dataReader.GetFloat(ordinal); + + public override Guid GetGuid(int ordinal) => dataReader.GetGuid(ordinal); + + public override short GetInt16(int ordinal) => dataReader.GetInt16(ordinal); + + public override int GetInt32(int ordinal) => dataReader.GetInt32(ordinal); + + public override long GetInt64(int ordinal) => dataReader.GetInt64(ordinal); + + public override string GetName(int ordinal) => dataReader.GetName(ordinal); + + public override int GetOrdinal(string name) => dataReader.GetOrdinal(name); + + public override string GetString(int ordinal) => dataReader.GetString(ordinal); + + public override object GetValue(int ordinal) => dataReader.GetValue(ordinal); + + public override int GetValues(object[] values) => dataReader.GetValues(values); + + public override bool IsDBNull(int ordinal) => dataReader.IsDBNull(ordinal); + + public override bool NextResult() => dataReader.NextResult(); + + public override bool Read() => dataReader.Read(); + + public override DataTable? GetSchemaTable() => dataReader.GetSchemaTable(); + + protected override DbDataReader GetDbDataReader(int ordinal) + { + return base.GetDbDataReader(ordinal); + } + +#if NET6_0_OR_GREATER + public override Task GetSchemaTableAsync(CancellationToken cancellationToken = default) + => dataReader.GetSchemaTableAsync(cancellationToken); +#endif +} diff --git a/src/LightORM/SqlExecutor/InternalReader.cs b/src/LightORM/SqlExecutor/InternalReader.cs index 586790a2..56d14c91 100644 --- a/src/LightORM/SqlExecutor/InternalReader.cs +++ b/src/LightORM/SqlExecutor/InternalReader.cs @@ -1,4 +1,5 @@ -using System.Collections; +using LightORM.Performances; +using System.Collections; using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Threading; diff --git a/src/LightORM/SqlExecutor/SqlExecutor.Prepare.cs b/src/LightORM/SqlExecutor/OrignalSqlExecutor.Prepare.cs similarity index 91% rename from src/LightORM/SqlExecutor/SqlExecutor.Prepare.cs rename to src/LightORM/SqlExecutor/OrignalSqlExecutor.Prepare.cs index 15272a58..28d165d3 100644 --- a/src/LightORM/SqlExecutor/SqlExecutor.Prepare.cs +++ b/src/LightORM/SqlExecutor/OrignalSqlExecutor.Prepare.cs @@ -3,7 +3,7 @@ namespace LightORM.SqlExecutor; -internal partial class SqlExecutor +internal partial class OrignalSqlExecutor { private void DisposeCommand(CommandResult result) { @@ -44,12 +44,12 @@ private CommandResult PrepareCommand(CommandType commandType, SqlExecuteContext conn.Open(); } var command = conn.CreateCommand(); - Database.DatabaseAdapter.DbCommandInit(command); + Provider.DatabaseAdapter.DbCommandInit(command); if (context != null) { command.Transaction = context.Transaction; } - et.HandleDbParameter(Database.DatabaseAdapter.Prefix, command); + et.HandleDbParameter(Provider.DatabaseAdapter.Prefix, command); return new(command, conn, needToReturn, false); } @@ -83,7 +83,7 @@ private async Task PrepareCommandAsync(CommandType commandType, S } var command = conn.CreateCommand(); - Database.DatabaseAdapter.DbCommandInit(command); + Provider.DatabaseAdapter.DbCommandInit(command); if (context != null) { @@ -96,7 +96,7 @@ private async Task PrepareCommandAsync(CommandType commandType, S // var action = DbParameterReader.GetDbParameterReader(Database.DatabaseAdapter.Prefix, commandText, dbParameters.GetType()); // action?.Invoke(command, dbParameters); //} - et.HandleDbParameter(Database.DatabaseAdapter.Prefix, command); + et.HandleDbParameter(Provider.DatabaseAdapter.Prefix, command); return new(command, conn, needToReturn, false); } diff --git a/src/LightORM/SqlExecutor/SqlExecutor.Trans.cs b/src/LightORM/SqlExecutor/OrignalSqlExecutor.Trans.cs similarity index 99% rename from src/LightORM/SqlExecutor/SqlExecutor.Trans.cs rename to src/LightORM/SqlExecutor/OrignalSqlExecutor.Trans.cs index c8727408..557add94 100644 --- a/src/LightORM/SqlExecutor/SqlExecutor.Trans.cs +++ b/src/LightORM/SqlExecutor/OrignalSqlExecutor.Trans.cs @@ -5,7 +5,7 @@ namespace LightORM.SqlExecutor; -internal partial class SqlExecutor +internal partial class OrignalSqlExecutor { // 事务上下文管理 internal static readonly ConcurrentDictionary> AsyncLocalTransactionContexts = new(); diff --git a/src/LightORM/SqlExecutor/SqlExecutor.cs b/src/LightORM/SqlExecutor/OrignalSqlExecutor.cs similarity index 88% rename from src/LightORM/SqlExecutor/SqlExecutor.cs rename to src/LightORM/SqlExecutor/OrignalSqlExecutor.cs index 2ff7c9cd..9827f8b2 100644 --- a/src/LightORM/SqlExecutor/SqlExecutor.cs +++ b/src/LightORM/SqlExecutor/OrignalSqlExecutor.cs @@ -1,16 +1,17 @@ -using System.Collections.Concurrent; +using LightORM.Performances; +using System.Collections.Concurrent; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; namespace LightORM.SqlExecutor; -internal partial class SqlExecutor : ISqlExecutor +[Obsolete] +internal partial class OrignalSqlExecutor : ISqlExecutor { public string Id { get; set; } - internal static readonly ConcurrentDictionary Pools = []; private static readonly ConcurrentDictionary PoolSizes = []; - public IDatabaseProvider Database { get; private set; } + public IDatabaseProvider Provider { get; private set; } /// /// 数据库事务 /// @@ -22,30 +23,28 @@ public DbTransaction? DbTransaction public AdoInterceptor Interceptor { get; } public ConnectionPool Pool { get; } - public SqlExecutor(IDatabaseProvider database, int poolSize, AdoInterceptor interceptor, string? id = null) + public OrignalSqlExecutor(IDatabaseProvider provider, int poolSize, AdoInterceptor interceptor, string? id = null) { - Database = database; + Provider = provider; Interceptor = interceptor; - _ = PoolSizes.GetOrAdd(database, poolSize); - Pool = Pools.GetOrAdd(database, db => + Pool = ConnectionPool.Pools.GetOrAdd(provider, db => { - PoolSizes.TryGetValue(db, out var size); return new ConnectionPool(() => { var conn = db.DbProviderFactory.CreateConnection()!; conn.ConnectionString = db.MasterConnectionString; return conn; - }, size); + }, ExpressionSqlOptions.Instance.Value.PoolSize); }); Id = id ?? Guid.NewGuid().ToString(); - CurrentTransactionContext = AsyncLocalTransactionContexts.GetOrAdd(Database, new AsyncLocal()); + CurrentTransactionContext = AsyncLocalTransactionContexts.GetOrAdd(Provider, new AsyncLocal()); } - public SqlExecutor(IDatabaseProvider database, AdoInterceptor interceptor, string? id = null) + public OrignalSqlExecutor(IDatabaseProvider database, AdoInterceptor interceptor, string? id = null) { - Database = database; + Provider = database; Interceptor = interceptor; - Pool = Pools.GetOrAdd(database, db => + Pool = ConnectionPool.Pools.GetOrAdd(database, db => { PoolSizes.TryGetValue(db, out var size); return new ConnectionPool(() => @@ -56,7 +55,7 @@ public SqlExecutor(IDatabaseProvider database, AdoInterceptor interceptor, strin }, size); }); Id = id ?? Guid.NewGuid().ToString(); - CurrentTransactionContext = AsyncLocalTransactionContexts.GetOrAdd(Database, new AsyncLocal()); + CurrentTransactionContext = AsyncLocalTransactionContexts.GetOrAdd(Provider, new AsyncLocal()); } public int ExecuteNonQuery< @@ -246,7 +245,7 @@ public DataSet ExecuteDataSet< TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) { var ds = new DataSet(); - using var adapter = Database.DbProviderFactory.CreateDataAdapter(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); var ctx = new SqlExecuteContext(ExecuteMethod.DataSet, commandText, dbParameters, typeof(TParameter), commandType); CommandResult? commandResult = default; try @@ -289,7 +288,7 @@ public DataTable ExecuteDataTable< TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) { var ds = new DataTable(); - using var adapter = Database.DbProviderFactory.CreateDataAdapter(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); var ctx = new SqlExecuteContext(ExecuteMethod.DataTable, commandText, dbParameters, typeof(TParameter), commandType); CommandResult? commandResult = default; try @@ -511,7 +510,7 @@ public async Task ExecuteDataSetAsync< TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) { var ds = new DataSet(); - using var adapter = Database.DbProviderFactory.CreateDataAdapter(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); var ctx = new SqlExecuteContext(ExecuteMethod.DataSet, commandText, dbParameters, typeof(TParameter), commandType); CommandResult? commandResult = default; try @@ -554,7 +553,7 @@ public async Task ExecuteDataTableAsync< TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) { var ds = new DataTable(); - using var adapter = Database.DbProviderFactory.CreateDataAdapter(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); var ctx = new SqlExecuteContext(ExecuteMethod.DataTable, commandText, dbParameters, typeof(TParameter), commandType); CommandResult? commandResult = default; try @@ -590,41 +589,7 @@ public async Task ExecuteDataTableAsync< return ds; } - internal static T? ChangeType(object? value) - { - if (value is null || value is DBNull) - { - return default; - } - if (value is T typedValue) - { - return typedValue; - } - var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - var result = targetType switch - { - _ when targetType == typeof(string) => value.ToString(), - _ when targetType == typeof(int) => Convert.ToInt32(value), - _ when targetType == typeof(long) => Convert.ToInt64(value), - _ when targetType == typeof(short) => Convert.ToInt16(value), - _ when targetType == typeof(byte) => Convert.ToByte(value), - _ when targetType == typeof(decimal) => Convert.ToDecimal(value), - _ when targetType == typeof(double) => Convert.ToDouble(value), - _ when targetType == typeof(float) => Convert.ToSingle(value), - _ when targetType == typeof(bool) => Convert.ToBoolean(value), - _ when targetType == typeof(DateTime) => Convert.ToDateTime(value), - _ when targetType == typeof(Guid) => Guid.Parse(value.ToString()!), - _ when targetType == typeof(char) => Convert.ToChar(value), - _ when targetType.IsEnum => Enum.Parse(targetType, value.ToString()!, ignoreCase: true), - // 兜底——理论上不会走到这里 - _ => Convert.ChangeType(value, targetType) - }; - if (result is T finalResult) - { - return finalResult; - } - return default; - } + // private readonly static ConcurrentDictionary?> commandInitCache = []; // internal static Action? GetInit(DbCommand commandObject) @@ -686,7 +651,7 @@ public async Task ExecuteDataTableAsync< public object Clone() { - return new SqlExecutor(Database, Interceptor); + return new OrignalSqlExecutor(Provider, Interceptor); } private bool disposedValue; @@ -710,19 +675,3 @@ public void Dispose() GC.SuppressFinalize(this); } } - -internal class StopwatchHelper -{ - public static long GetTimestamp() => Stopwatch.GetTimestamp(); - public static TimeSpan GetElapsedTime(long startingTimestamp) - { -#if NET8_0_OR_GREATER - return Stopwatch.GetElapsedTime(startingTimestamp); -#else - var end = Stopwatch.GetTimestamp(); - var tickFrequency = (double)(10000 * 1000 / Stopwatch.Frequency); - var tick = (end - startingTimestamp) * tickFrequency; - return new TimeSpan((long)tick); -#endif - } -} \ No newline at end of file diff --git a/src/LightORM/SqlExecutor/SqlAdo.cs b/src/LightORM/SqlExecutor/SqlAdo.cs new file mode 100644 index 00000000..481dfd7d --- /dev/null +++ b/src/LightORM/SqlExecutor/SqlAdo.cs @@ -0,0 +1,748 @@ +using LightORM.Performances; +using System.Collections.Concurrent; +using System.Data.Common; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +namespace LightORM.SqlExecutor; + +internal readonly struct PrepareResult(DbCommand command, bool isBreak) +{ + public DbCommand Command { get; } = command; + public bool Break { get; } = isBreak; +} +public readonly struct SqlAdo +{ + internal SqlAdo(DatabaseConnection connection) + { + Connection = connection; + } + internal DatabaseConnection Connection { get; } + public IDatabaseProvider Provider => Connection.Provider; + internal AdoInterceptor Interceptor => Connection.Interceptor; + + internal void UseExternalTransaction(DbTransaction transaction) => Connection.UseExternalTransaction(transaction); + + internal void DisposeConnection() + { + if (Connection.UnderTransaction) + { + return; + } + Connection.Dispose(); + } + + private void DisposeCommand(PrepareResult result) + { + DisposeConnection(); + result.Command.Parameters.Clear(); + result.Command.Dispose(); + } + +#if NET8_0_OR_GREATER + + private ValueTask DisposeCommandAsync(PrepareResult result) + { + DisposeConnection(); + result.Command.Parameters.Clear(); + return result.Command.DisposeAsync(); + } +#endif + + #region prepare + private PrepareResult PrepareCommand(CommandType commandType, SqlExecuteContext et) + { + if (Connection.IsOccurException == true) + { + return new(null!, true); + } + //DbLog?.Invoke(commandText, dbParameters); + Interceptor.NotifyPrepareCommand(et); + DbConnection conn = Connection.GetCurrentConnection(); + if (conn.State != ConnectionState.Open) + { + conn.Open(); + } + var command = conn.CreateCommand(); + command.CommandType = commandType; + Provider.DatabaseAdapter.DbCommandInit(command); + if (Connection.Transaction is not null) + { + command.Transaction = Connection.Transaction; + } + et.HandleDbParameter(Provider.DatabaseAdapter.Prefix, command); + return new(command, false); + } + + private async Task PrepareCommandAsync(CommandType commandType, SqlExecuteContext et, CancellationToken cancellationToken = default) + { + if (Connection.IsOccurException == true) + { + return new(null!, true); + } + //DbLog?.Invoke(commandText, dbParameters); + Interceptor.NotifyPrepareCommand(et); + DbConnection conn = Connection.GetCurrentConnection(); + if (conn.State != ConnectionState.Open) + { + await conn.OpenAsync(cancellationToken).ConfigureAwait(false); + } + + var command = conn.CreateCommand(); + command.CommandType = commandType; + Provider.DatabaseAdapter.DbCommandInit(command); + + if (Connection.Transaction is not null) + { + command.Transaction = Connection.Transaction; + } + et.HandleDbParameter(Provider.DatabaseAdapter.Prefix, command); + return new(command, false); + } + #endregion + + #region execute + + public int ExecuteNonQuery< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) + { + var ctx = new SqlExecuteContext(ExecuteMethod.NonQuery, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return 0; + } + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + var result = r.Command.ExecuteNonQuery(); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return result; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return 0; + } + throw; + } + finally + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + } + } + + public ScalarValue ExecuteScalar< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Scalar, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return default; + } + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + var obj = r.Command.ExecuteScalar(); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new ScalarValue(obj); + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return default; + } + throw; + } + finally + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + } + } + + public DbDataReader ExecuteReader< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CommandBehavior? behavior = null) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Reader, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return new EmptyDataReader(); + } + DbDataReader reader; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + if (!Connection.UnderTransaction) + { + var b = behavior.HasValue ? behavior.Value | CommandBehavior.CloseConnection : CommandBehavior.CloseConnection; + reader = r.Command.ExecuteReader(b); + } + else + { + reader = r.Command.ExecuteReader(behavior ?? CommandBehavior.Default); + } + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new InternalDataReaderLight(reader, r, Connection); + } + catch (Exception ex) + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new EmptyDataReader(); + } + throw; + } + finally + { + + } + } + + public MultipleResult QueryMultiple< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CommandBehavior? behavior = null) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Reader, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return new(new EmptyDataReader()); + } + DbDataReader reader; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + if (behavior?.HasFlag(CommandBehavior.SingleResult) == true) + { + throw new LightOrmException("behavior 指定了 CommandBehavior.SingleResult, 不符合QueryMultiple的行为"); + } + if (!Connection.UnderTransaction) + { + var b = behavior.HasValue ? behavior.Value | CommandBehavior.CloseConnection : CommandBehavior.CloseConnection; + reader = r.Command.ExecuteReader(b); + + } + else + { + reader = r.Command.ExecuteReader(behavior ?? CommandBehavior.Default); + } + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new(new InternalDataReaderLight(reader, r, Connection)); + } + catch (Exception ex) + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new(new EmptyDataReader()); + } + throw; + } + finally + { + + } + } + + public DataSet ExecuteDataSet< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) + { + var ds = new DataSet(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); + var ctx = new SqlExecuteContext(ExecuteMethod.DataSet, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return new(); + } + adapter!.SelectCommand = r.Command; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + adapter.Fill(ds); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return ds; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new DataSet(); + } + throw; + } + finally + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + } + } + + public DataTable ExecuteDataTable< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text) + { + var ds = new DataTable(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); + var ctx = new SqlExecuteContext(ExecuteMethod.DataTable, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = PrepareCommand(commandType, ctx); + var r = commandResult.Value; + if (r.Break) + { + return new(); + } + adapter!.SelectCommand = r.Command; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + adapter.Fill(ds); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return ds; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new DataTable(); + } + throw; + } + finally + { + if (commandResult.HasValue) + { + DisposeCommand(commandResult.Value); + } + } + } + + public async Task ExecuteNonQueryAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) + { + var ctx = new SqlExecuteContext(ExecuteMethod.NonQuery, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return 0; + } + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + var result = await r.Command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return result; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return 0; + } + throw; + } + finally + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + } + } + + public async Task ExecuteScalarAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Scalar, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return default; + } + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + var obj = await r.Command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new(obj); + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return default; + } + throw; + } + finally + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + } + } + public async Task ExecuteReaderAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CommandBehavior? behavior = null, CancellationToken cancellationToken = default) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Reader, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return new EmptyDataReader(); + } + DbDataReader reader; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + if (!Connection.UnderTransaction) + { + var b = behavior.HasValue ? behavior.Value | CommandBehavior.CloseConnection : CommandBehavior.CloseConnection; + reader = await r.Command.ExecuteReaderAsync(b, cancellationToken).ConfigureAwait(false); + } + else + { + reader = await r.Command.ExecuteReaderAsync(behavior ?? CommandBehavior.Default, cancellationToken).ConfigureAwait(false); + } + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new InternalDataReaderLight(reader, r, Connection); + } + catch (Exception ex) + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new EmptyDataReader(); + } + throw; + } + finally + { + + } + } + + public async Task QueryMultipleAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CommandBehavior? behavior = null, CancellationToken cancellationToken = default) + { + var ctx = new SqlExecuteContext(ExecuteMethod.Reader, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return new(new EmptyDataReader()); + } + DbDataReader reader; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + if (behavior?.HasFlag(CommandBehavior.SingleResult) == true) + { + throw new LightOrmException("behavior 指定了 CommandBehavior.SingleResult, 不符合QueryMultiple的行为"); + } + if (!Connection.UnderTransaction) + { + var b = behavior.HasValue ? behavior.Value | CommandBehavior.CloseConnection : CommandBehavior.CloseConnection; + reader = await r.Command.ExecuteReaderAsync(b, cancellationToken).ConfigureAwait(false); + + } + else + { + reader = await r.Command.ExecuteReaderAsync(behavior ?? CommandBehavior.Default, cancellationToken).ConfigureAwait(false); + } + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return new(new InternalDataReaderLight(reader, r, Connection)); + } + catch (Exception ex) + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return new(new EmptyDataReader()); + } + throw; + } + finally + { + + } + } + + public async Task ExecuteDataSetAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) + { + var ds = new DataSet(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); + var ctx = new SqlExecuteContext(ExecuteMethod.DataSet, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return ds; + } + adapter!.SelectCommand = r.Command; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + adapter.Fill(ds); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return ds; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return ds; + } + throw; + } + finally + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + } + } + + public async Task ExecuteDataTableAsync< +#if NET8_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +#endif + TParameter>(string commandText, TParameter dbParameters, CommandType commandType = CommandType.Text, CancellationToken cancellationToken = default) + { + var ds = new DataTable(); + using var adapter = Provider.DbProviderFactory.CreateDataAdapter(); + var ctx = new SqlExecuteContext(ExecuteMethod.DataTable, commandText, dbParameters, typeof(TParameter), commandType); + PrepareResult? commandResult = default; + try + { + commandResult = await PrepareCommandAsync(commandType, ctx, cancellationToken).ConfigureAwait(false); + var r = commandResult.Value; + if (r.Break) + { + return ds; + } + adapter!.SelectCommand = r.Command; + Interceptor.NotifyBeforeExecute(ctx); + var start = StopwatchHelper.GetTimestamp(); + adapter.Fill(ds); + ctx.Elapsed = StopwatchHelper.GetElapsedTime(start); + Interceptor.NotifyAfterExecute(ctx); + return ds; + } + catch (Exception ex) + { + var ectx = new SqlExecuteExceptionContext(ctx, ex); + Interceptor.NotifyException(ectx); + if (ectx.IsHandled) + { + return ds; + } + throw; + } + finally + { + if (commandResult.HasValue) + { +#if NET8_0_OR_GREATER + await DisposeCommandAsync(commandResult.Value).ConfigureAwait(false); +#else + DisposeCommand(commandResult.Value); +#endif + } + } + } + + #endregion + + internal static T? ChangeType(object? value) + { + if (value is null || value is DBNull) + { + return default; + } + if (value is T typedValue) + { + return typedValue; + } + var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var result = targetType switch + { + _ when targetType == typeof(string) => value.ToString(), + _ when targetType == typeof(int) => Convert.ToInt32(value), + _ when targetType == typeof(long) => Convert.ToInt64(value), + _ when targetType == typeof(short) => Convert.ToInt16(value), + _ when targetType == typeof(byte) => Convert.ToByte(value), + _ when targetType == typeof(decimal) => Convert.ToDecimal(value), + _ when targetType == typeof(double) => Convert.ToDouble(value), + _ when targetType == typeof(float) => Convert.ToSingle(value), + _ when targetType == typeof(bool) => Convert.ToBoolean(value), + _ when targetType == typeof(DateTime) => Convert.ToDateTime(value), + _ when targetType == typeof(Guid) => Guid.Parse(value.ToString()!), + _ when targetType == typeof(char) => Convert.ToChar(value), + _ when targetType.IsEnum => Enum.Parse(targetType, value.ToString()!, ignoreCase: true), + // 兜底——理论上不会走到这里 + _ => Convert.ChangeType(value, targetType) + }; + if (result is T finalResult) + { + return finalResult; + } + return default; + } +} + +internal class StopwatchHelper +{ + public static long GetTimestamp() => Stopwatch.GetTimestamp(); + public static TimeSpan GetElapsedTime(long startingTimestamp) + { +#if NET8_0_OR_GREATER + return Stopwatch.GetElapsedTime(startingTimestamp); +#else + var end = Stopwatch.GetTimestamp(); + var tickFrequency = (double)(10000 * 1000 / Stopwatch.Frequency); + var tick = (end - startingTimestamp) * tickFrequency; + return new TimeSpan((long)tick); +#endif + } +} \ No newline at end of file diff --git a/src/LightORM/Utils/SqlExecutorProvider.cs b/src/LightORM/Utils/SqlExecutorProvider.cs index 22d42898..fa0c100d 100644 --- a/src/LightORM/Utils/SqlExecutorProvider.cs +++ b/src/LightORM/Utils/SqlExecutorProvider.cs @@ -1,89 +1,114 @@ -using System.Collections.Concurrent; +using LightORM.Performances; +using System.Collections.Concurrent; -namespace LightORM.Utils +namespace LightORM.Utils; + +internal class ConnectionFactory(ExpressionSqlOptions option) { - internal class SqlExecutorProvider : IDisposable + public DatabaseConnection GetDatabaseConnection(string key) { - //public static ISqlExecutor GetExecutor(string key = ConstString.Main) - //{ - // var dbInfo = StaticCache.Get(key) ?? throw new LightOrmException($"{key} not register"); - // return new SqlExecutor.SqlExecutor(dbInfo, 5); - //} - - public IDatabaseProvider GetDbInfo(string key) + var provider = GetDbInfo(key, option); + var pool = ConnectionPool.Pools.GetOrAdd(provider, p => { - return option.DatabaseProviders.TryGetValue(key, out var db) ? db : throw new ArgumentException($"{key} not register"); - } + return new ConnectionPool(() => + { + var conn = p.DbProviderFactory.CreateConnection()!; + conn.ConnectionString = p.MasterConnectionString; + return conn; + }, option.PoolSize); + }); + var conn = pool.Get(); + return new DatabaseConnection(conn, provider,new(option.Interceptors)); + } - Func? customHandler; - private readonly ConcurrentDictionary executors = []; - private readonly ExpressionSqlOptions option; - public SqlExecutorProvider(ExpressionSqlOptions option) + public DatabaseConnection GetDatabaseConnection(IDatabaseProvider provider) + { + var pool = ConnectionPool.Pools.GetOrAdd(provider, p => { - this.option = option; - } + return new ConnectionPool(() => + { + var conn = p.DbProviderFactory.CreateConnection()!; + conn.ConnectionString = p.MasterConnectionString; + return conn; + }, option.PoolSize); + }); + var conn = pool.Get(); + return new DatabaseConnection(conn, provider, new(option.Interceptors)); + } + public static IDatabaseProvider GetDbInfo(string key, ExpressionSqlOptions option) + { + return option.DatabaseProviders.TryGetValue(key, out var db) ? db : throw new ArgumentException($"{key} not register"); + } +} - public void UseCustomExecutor(Func customHandler) - { - this.customHandler = customHandler; - } +[Obsolete] +internal class SqlExecutorProvider : IDisposable +{ + //public static ISqlExecutor GetExecutor(string key = ConstString.Main) + //{ + // var dbInfo = StaticCache.Get(key) ?? throw new LightOrmException($"{key} not register"); + // return new SqlExecutor.SqlExecutor(dbInfo, 5); + //} - private ISqlExecutor? CreateCustomExecutor() - { - if (customHandler == null) return null; - var e = customHandler.Invoke(); - customHandler = null; - return e; - } + public IDatabaseProvider GetDbInfo(string key) + { + return option.DatabaseProviders.TryGetValue(key, out var db) ? db : throw new ArgumentException($"{key} not register"); + } - public ConcurrentDictionary Executors => executors; + private readonly ConcurrentDictionary executors = []; + private readonly ExpressionSqlOptions option; + public SqlExecutorProvider(ExpressionSqlOptions option) + { + this.option = option; + } - public ISqlExecutor GetSqlExecutor(string key = ConstString.Main) => CreateCustomExecutor() ?? InternalCreator(key); + public ConcurrentDictionary Executors => executors; - private ISqlExecutor InternalCreator(string key) + public ISqlExecutor GetSqlExecutor(string key = ConstString.Main) => InternalCreator(key); + + private ISqlExecutor InternalCreator(string key) + { + return executors.GetOrAdd(key, k => { - return executors.GetOrAdd(key, k => - { - var ado = new SqlExecutor.SqlExecutor(GetDbInfo(k), option.PoolSize, new AdoInterceptor(option.Interceptors), k); - //if (useTrans) - //{ - // ado.BeginTran(); - //} - return ado; - }); - } + var ado = new SqlExecutor.OrignalSqlExecutor(GetDbInfo(k), option.PoolSize, new AdoInterceptor(option.Interceptors), k); + //if (useTrans) + //{ + // ado.BeginTran(); + //} + return ado; + }); + } - #region dispose - private bool disposedValue; + #region dispose + private bool disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) { - if (!disposedValue) + if (disposing) { - if (disposing) + foreach (var item in executors.Values) { - foreach (var item in executors.Values) - { - item.Dispose(); - } - executors.Clear(); - //foreach (var item in queryExecutors) - //{ - // item?.Dispose(); - //} - //queryExecutors.Clear(); + item.Dispose(); } - disposedValue = true; + executors.Clear(); + //foreach (var item in queryExecutors) + //{ + // item?.Dispose(); + //} + //queryExecutors.Clear(); } + disposedValue = true; } + } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - #endregion + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + #endregion } diff --git a/src/LightOrmExtensionGenerator/SelectExtensionGenerator.cs b/src/LightOrmExtensionGenerator/SelectExtensionGenerator.cs index 9f7e8f19..8436944b 100644 --- a/src/LightOrmExtensionGenerator/SelectExtensionGenerator.cs +++ b/src/LightOrmExtensionGenerator/SelectExtensionGenerator.cs @@ -32,7 +32,7 @@ public static IExpSelect<{{argsStr}}> Select< var key = GetDbKey({{string.Join(", ", types)}}); if (key != null) { - return new SelectProvider{{count}}<{{argsStr}}>(instance.SwitchDatabase(key)); + return new SelectProvider{{count}}<{{argsStr}}>(instance.CreateScoped(key)); } return new SelectProvider{{count}}<{{argsStr}}>(instance); } @@ -64,7 +64,7 @@ public static IExpSelect<{{argsStr}}> Select< public static DataTable ToDataTable<{{argsStr}}>(this IExpSelect<{{argsStr}}> select, Expression> exp) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTable(sql, parameters); } @@ -72,7 +72,7 @@ public static IExpSelect<{{argsStr}}> Select< public static Task ToDataTableAsync<{{argsStr}}>(this IExpSelect<{{argsStr}}> select, Expression> exp, CancellationToken cancellationToken = default) { select.HandleResult(exp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTableAsync(sql, parameters, cancellationToken: cancellationToken); } @@ -110,7 +110,7 @@ public static IExpSelect<{{argsStr}}> Select< { var flatExp = FlatTypeSet.Default.Flat(exp)!; select.HandleResult(flatExp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTable(sql, parameters); } @@ -119,7 +119,7 @@ public static IExpSelect<{{argsStr}}> Select< { var flatExp = FlatTypeSet.Default.Flat(exp)!; select.HandleResult(flatExp, null); - var sql = select.SqlBuilder.ToSqlString(select.Executor.Database.DatabaseAdapter); + var sql = select.SqlBuilder.ToSqlString(select.Executor.Provider.DatabaseAdapter); var parameters = select.SqlBuilder.DbParameters; return select.Executor.ExecuteDataTableAsync(sql, parameters, cancellationToken: cancellationToken); } diff --git a/src/versions.props b/src/versions.props index cf4dd3fb..1bbec9c9 100644 --- a/src/versions.props +++ b/src/versions.props @@ -1,11 +1,11 @@  - 2026.08.21.1 + 2026.08.26.1 - 2026.08.21.1 + 2026.08.26.1 - 2026.08.21.1 + 2026.08.26.1 2026.08.21.1 diff --git a/test/BenchmarkTest/StringBuilderPoolTest.cs b/test/BenchmarkTest/StringBuilderPoolTest.cs index 896db361..fc4a37ad 100644 --- a/test/BenchmarkTest/StringBuilderPoolTest.cs +++ b/test/BenchmarkTest/StringBuilderPoolTest.cs @@ -35,7 +35,7 @@ public string SimpleSelectNoPool() var db = ExpSqlFactory.GetContext(); var select = db.Select(); var builder = select.SqlBuilder; - var database = select.Executor.Database.DatabaseAdapter; + var database = select.Executor.Provider.DatabaseAdapter; return builder.ToSqlString(database); } @@ -45,7 +45,7 @@ public string SimpleSelectWithPool() var db = ExpSqlFactory.GetContext(); var select = db.Select(); var builder = select.SqlBuilder; - var database = select.Executor.Database.DatabaseAdapter; + var database = select.Executor.Provider.DatabaseAdapter; return builder.ToSqlString(database); } @@ -117,7 +117,7 @@ public string CTENoPool() t }); var builder = select.SqlBuilder; - var database = select.Executor.Database.DatabaseAdapter; + var database = select.Executor.Provider.DatabaseAdapter; return builder.ToSqlString(database); } @@ -189,7 +189,7 @@ public string CTEWithPool() t }); var builder = select.SqlBuilder; - var database = select.Executor.Database.DatabaseAdapter; + var database = select.Executor.Provider.DatabaseAdapter; return builder.ToSqlString(database); } diff --git a/test/LightORMTest/ResultTest/ExecutionTest.Trans.cs b/test/LightORMTest/ResultTest/ExecutionTest.Trans.cs new file mode 100644 index 00000000..b67bc775 --- /dev/null +++ b/test/LightORMTest/ResultTest/ExecutionTest.Trans.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Text; + +namespace LightORMTest.ResultTest; + +public partial class ExecutionTest +{ + [TestMethod] + public async Task Transaction_BasicCommit_Test() + { + // Arrange + var userId = "test01"; + var originalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + var originalAge = originalUser?.Age; + + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // Act + await scope.Update() + .Set(u => u.Age, originalAge + 10) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Assert - 验证提交成功 + var updatedUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual(originalAge + 10, updatedUser?.Age, "事务提交后数据应该被更新"); + } + catch + { + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + } + + [TestMethod] + public async Task Transaction_Rollback_Test() + { + // Arrange + var userId = "test01"; + var originalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + var originalAge = originalUser?.Age; + + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // Act - 执行更新 + await scope.Update() + .Set(u => u.Age, originalAge + 999) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 回滚事务 + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Assert - 验证回滚成功 + var rolledBackUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual(originalAge, rolledBackUser?.Age, "事务回滚后数据应该恢复原值"); + } + catch + { + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + } + + [TestMethod] + public async Task Transaction_Atomicity_Test() + { + // Arrange + var userId = "test01"; + var originalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + var originalAge = originalUser!.Age; + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Act & Assert + try + { + // 操作1: 更新用户 + await scope.Update() + .Set(u => u.Age, originalAge + 100) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 操作2: 插入无效数据(故意触发异常) + await scope.Insert(new User() + { + UserId = "invalid!@#", + UserName = new string('c', 500),// 超出最大长度限制 + Age = 999 + }).ExecuteAsync(TestContext.CancellationToken); + + // 如果执行到这里,说明插入应该失败,但失败了就不会走到这里 + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // 如果事务提交,验证数据 + var finalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual(originalAge + 100, finalUser?.Age); + } + catch (Exception ex) + { + // 回滚事务 + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // 验证数据没有被修改 + var rolledBackUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual(originalAge, rolledBackUser?.Age, "事务异常回滚后数据应该恢复"); + + Console.WriteLine($"事务回滚成功: {ex.Message}"); + return; + } + + Assert.Fail("应该抛出异常并回滚事务"); + } + + [TestMethod] + public async Task Transaction_MultiTable_Test() + { + // Arrange + var userId = "test01"; + var roleId = "Admin"; + + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // Act - 同时操作多个表 + // 1. 更新用户信息 + await scope.Update() + .Set(u => u.UserName, "UpdatedName") + .Set(u => u.Age, 99) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 2. 添加新的用户角色 + await scope.Insert(new UserRole() + { + UserId = userId, + RoleId = "SuperAdmin" + }).ExecuteAsync(TestContext.CancellationToken); + + // 3. 更新角色名称 + await scope.Update() + .Set(r => r.RoleName, "超级管理员(已更新)") + .Where(r => r.RoleId == roleId) + .ExecuteAsync(TestContext.CancellationToken); + + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Assert - 验证所有操作都成功 + var user = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual("UpdatedName", user?.UserName); + Assert.AreEqual(99, user?.Age); + + var roles = await Db.Select().Where(ur => ur.UserId == userId).ToListAsync(TestContext.CancellationToken); + Assert.IsTrue(roles.Any(r => r.RoleId == "SuperAdmin"), "SuperAdmin 角色应该被添加"); + + var role = await Db.Select().Where(r => r.RoleId == roleId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual("超级管理员(已更新)", role?.RoleName); + } + catch + { + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + } + + [TestMethod] + public async Task Transaction_Nested_Test() + { + // Arrange + var userId = "test01"; + + using var scope = Db.CreateScoped(); + + // 外层事务 + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // 外层操作 + await scope.Update() + .Set(u => u.UserName, "OuterUpdate") + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 嵌套事务 - 开启 Savepoint + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); // 嵌套开启 + + try + { + // 内层操作 + await scope.Update() + .Set(u => u.Age, 888) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 内层提交(实际只是减少嵌套计数) + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + } + catch + { + // 内层回滚(回滚到 Savepoint) + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + + // 外层提交(真实提交) + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Assert - 验证所有更改生效 + var user = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual("OuterUpdate", user?.UserName); + Assert.AreEqual(888, user?.Age); + } + catch + { + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + } + + [TestMethod] + public async Task Transaction_Concurrency_Test() + { + // Arrange + var userId = "test01"; + var initialUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + + // 使用信号量控制并发 + var semaphore = new SemaphoreSlim(5); // 限制并发数 + var errors = new ConcurrentBag(); + var successCount = 0; + var conflictCount = 0; + + await Parallel.ForAsync(0, 20, async (i, ct) => + { + await semaphore.WaitAsync(ct); + try + { + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(IsolationLevel.RepeatableRead, TestContext.CancellationToken); + + // 读取当前值 + var currentUser = await scope.Select() + .Where(u => u.UserId == userId) + .FirstAsync(ct); + + // 模拟业务延迟增加并发冲突 + await Task.Delay(Random.Shared.Next(10, 50), ct); + + // 使用乐观锁更新 + var newAge = currentUser?.Age + 1; + var affected = await scope.Update() + .Set(u => u.Age, newAge) + .WithVersion(u => u.Version, currentUser?.Version) + .Where(u => u.UserId == userId) // 乐观锁 + .ExecuteAsync(ct); + + if (affected > 0) + { + Interlocked.Increment(ref successCount); + Console.WriteLine($"任务{i}: 更新成功, Age = {newAge}"); + } + else + { + Interlocked.Increment(ref conflictCount); + Console.WriteLine($"任务{i}: 并发冲突, 版本号不匹配"); + } + + await scope.CommitTransactionAsync("MainDb", TestContext.CancellationToken); + } + catch (Exception ex) + { + errors.Add(ex); + Console.WriteLine($"任务{i} 异常: {ex.Message}"); + } + finally + { + semaphore.Release(); + } + }); + + // Assert + Assert.IsEmpty(errors, $"发生异常: {string.Join(Environment.NewLine, errors.Select(e => e.Message))}"); + Assert.IsGreaterThan(0, successCount, "至少应该有一次成功更新"); + Assert.IsGreaterThan(0, conflictCount, "应该检测到并发冲突"); + + // 验证最终数据一致性 + var finalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + var expectedAge = initialUser?.Age + successCount; + Assert.AreEqual(expectedAge, finalUser?.Age, $"最终年龄应为 {initialUser?.Age} + {successCount} = {expectedAge}"); + } + + [TestMethod] + public async Task Transaction_WithInclude_Test() + { + // Arrange + var userId = "test01"; + + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // Act - 在事务中查询 Include 数据 + var user = await scope.Select() + .Include(u => u.UserRoles) + .Where(u => u.UserId == userId) + .FirstAsync(TestContext.CancellationToken); + + // 在事务中更新 + await scope.Update() + .Set(u => u.UserName, "TransactionWithInclude") + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 验证 Include 数据正确 + Assert.IsNotNull(user); + Assert.IsNotNull(user.UserRoles); + Assert.IsTrue(user.UserRoles.Any()); + + foreach (var ur in user.UserRoles) + { + Assert.IsNotNull(ur); + Console.WriteLine($"User: {user.UserName}, Role: {ur.RoleName}"); + } + + await scope.CommitTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // Assert - 验证更新生效 + var updatedUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual("TransactionWithInclude", updatedUser?.UserName); + } + catch + { + await scope.RollbackTransactionAsync(cancellationToken: TestContext.CancellationToken); + throw; + } + } + + [TestMethod] + public async Task Transaction_CleanupOnException_Test() + { + // Arrange + var userId = "test01"; + var originalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + var originalAge = originalUser?.Age; + + // Act - 不使用 using,手动管理 + var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + try + { + // 执行更新 + await scope.Update() + .Set(u => u.Age, originalAge + 1000) + .Where(u => u.UserId == userId) + .ExecuteAsync(TestContext.CancellationToken); + + // 故意抛出异常 + throw new InvalidOperationException("模拟业务异常"); + } + catch (Exception ex) + { + Console.WriteLine($"捕获异常: {ex.Message}"); + // 注意:这里没有调用 Rollback 或 Commit + // 依赖 scope 的 Dispose 自动处理 + } + finally + { + // 手动释放(会触发自动回滚) + scope.Dispose(); + } + + // Assert - 验证数据没有被修改 + var finalUser = await Db.Select().Where(u => u.UserId == userId).FirstAsync(TestContext.CancellationToken); + Assert.AreEqual(originalAge, finalUser?.Age, "异常发生后数据应该保持不变"); + } + + [TestMethod] + public async Task Transaction_Cancellation_Test() + { + var userId = "test01"; + + using var scope = Db.CreateScoped(); + await scope.BeginTransactionAsync(cancellationToken: TestContext.CancellationToken); + + // 使用 CancellationTokenSource 模拟超时 + var timeout = Random.Shared.Next(1, 20); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); + + try + { + // 执行一个长时间操作 + await scope.Update() + .Set(u => u.UserName, "CancellationTest") + .Where(u => u.UserId == userId) + .ExecuteAsync(cts.Token); + await scope.CommitTransactionAsync(cts.Token); + Console.WriteLine("操作完成"); + } + catch (OperationCanceledException) + { + // 操作被取消 + await scope.RollbackTransactionAsync(cts.Token); + Console.WriteLine("操作被取消,事务已回滚"); + return; + } + } +} diff --git a/test/LightORMTest/SwitchDatabaseTest.cs b/test/LightORMTest/SwitchDatabaseTest.cs index 205cf382..db39b85f 100644 --- a/test/LightORMTest/SwitchDatabaseTest.cs +++ b/test/LightORMTest/SwitchDatabaseTest.cs @@ -40,21 +40,21 @@ public void TestConfig() Console.WriteLine(db1); } - [TestMethod] - public async Task TestSwitchDatabase() - { - var random = new Random(); - await Parallel.ForAsync(0, 100, async (_, c) => - { - for (int i = 0; i < 20; i++) - { - var index = random.Next(1, 6); - var wait = random.Next(1, 50); - var dbKey = $"db{index}"; - Assert.IsTrue(Db.SwitchDatabase(dbKey).Key == dbKey); - await Task.Delay(wait, c); - } - }); - } + //[TestMethod] + //public async Task TestSwitchDatabase() + //{ + // var random = new Random(); + // await Parallel.ForAsync(0, 100, async (_, c) => + // { + // for (int i = 0; i < 20; i++) + // { + // var index = random.Next(1, 6); + // var wait = random.Next(1, 50); + // var dbKey = $"db{index}"; + // Assert.IsTrue(Db.SwitchDatabase(dbKey).Key == dbKey); + // await Task.Delay(wait, c); + // } + // }); + //} } diff --git a/test/LightORMTest/TestBase.cs b/test/LightORMTest/TestBase.cs index 7ff68470..2163c5e0 100644 --- a/test/LightORMTest/TestBase.cs +++ b/test/LightORMTest/TestBase.cs @@ -26,7 +26,7 @@ protected TestBase() { Configura(option); option.UseInterceptor(); - //option.SetTableContext(TableContext); + option.SetTableContext(TableContext); option.ConfigJsonHandler(); });