From 3581ce87088c31d251bc3e0808ddf6745d17a87e Mon Sep 17 00:00:00 2001 From: "zzzwangjun@gmail.com" <510423039@qq.com> Date: Wed, 24 Sep 2025 11:01:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20mysql=E6=89=B9=E9=87=8F=E6=8F=92?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aspnet-core/Lion.AbpPro.sln | 7 + .../GlobalUsings.cs | 1 - ...on.AbpPro.EntityFrameworkCore.Mysql.csproj | 8 +- .../AbpProEntityFrameworkCoreMysqlModule.cs | 5 +- .../Mysql/EfCoreBulkOperationProvider.cs | 15 +- .../System/Linq/EfDapperBulkExtensions.cs | 196 ++++++++++++++++++ .../System/Linq/MySQLBulkInsertExtensions.cs | 45 ---- .../DataDictionaries/DataDictionaryManager.cs | 24 ++- .../AbpProHttpApiHostModule.cs | 5 +- .../Lion.AbpPro.HttpApi.Host.csproj | 1 + 10 files changed, 240 insertions(+), 67 deletions(-) create mode 100644 aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/EfDapperBulkExtensions.cs delete mode 100644 aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/MySQLBulkInsertExtensions.cs diff --git a/aspnet-core/Lion.AbpPro.sln b/aspnet-core/Lion.AbpPro.sln index 0682fb75..a6175dad 100644 --- a/aspnet-core/Lion.AbpPro.sln +++ b/aspnet-core/Lion.AbpPro.sln @@ -255,6 +255,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lion.AbpPro.Hangfire", "fra EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lion.AbpPro.SignalR", "Lion.AbpPro.SignalR\Lion.AbpPro.SignalR.csproj", "{66B3D9E0-CB3F-464A-9813-F2DC1426A37A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lion.AbpPro.EntityFrameworkCore.Mysql", "frameworks\src\Lion.AbpPro.EntityFrameworkCore.Mysql\Lion.AbpPro.EntityFrameworkCore.Mysql.csproj", "{AF650F62-0447-4739-B73D-44E0120E0275}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -629,6 +631,10 @@ Global {66B3D9E0-CB3F-464A-9813-F2DC1426A37A}.Debug|Any CPU.Build.0 = Debug|Any CPU {66B3D9E0-CB3F-464A-9813-F2DC1426A37A}.Release|Any CPU.ActiveCfg = Release|Any CPU {66B3D9E0-CB3F-464A-9813-F2DC1426A37A}.Release|Any CPU.Build.0 = Release|Any CPU + {AF650F62-0447-4739-B73D-44E0120E0275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF650F62-0447-4739-B73D-44E0120E0275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF650F62-0447-4739-B73D-44E0120E0275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF650F62-0447-4739-B73D-44E0120E0275}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -751,6 +757,7 @@ Global {89CCAEA6-8176-4E4B-8D84-A2ACE2715F88} = {7BE85EBC-99AD-4CDE-957E-4BDD087FC4E3} {6C2FDD3D-F711-46B0-A2F2-B94BC33F136B} = {7BE85EBC-99AD-4CDE-957E-4BDD087FC4E3} {66B3D9E0-CB3F-464A-9813-F2DC1426A37A} = {7BE85EBC-99AD-4CDE-957E-4BDD087FC4E3} + {AF650F62-0447-4739-B73D-44E0120E0275} = {7BE85EBC-99AD-4CDE-957E-4BDD087FC4E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F} diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/GlobalUsings.cs b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/GlobalUsings.cs index 80deef1d..dd566892 100644 --- a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/GlobalUsings.cs +++ b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/GlobalUsings.cs @@ -3,7 +3,6 @@ global using Lion.AbpPro.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Storage; -global using MySqlConnector; global using Volo.Abp.DependencyInjection; global using Volo.Abp.Domain.Entities; global using Volo.Abp.Domain.Repositories.EntityFrameworkCore; diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion.AbpPro.EntityFrameworkCore.Mysql.csproj b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion.AbpPro.EntityFrameworkCore.Mysql.csproj index 0e3e5182..10f249c9 100644 --- a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion.AbpPro.EntityFrameworkCore.Mysql.csproj +++ b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion.AbpPro.EntityFrameworkCore.Mysql.csproj @@ -1,16 +1,12 @@ - + net9.0 enable - - - - - + diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/AbpProEntityFrameworkCoreMysqlModule.cs b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/AbpProEntityFrameworkCoreMysqlModule.cs index 4da08e67..4d5f67a9 100644 --- a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/AbpProEntityFrameworkCoreMysqlModule.cs +++ b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/AbpProEntityFrameworkCoreMysqlModule.cs @@ -1,6 +1,9 @@ -namespace Lion.AbpPro.EntityFrameworkCore.Mysql; +using Volo.Abp.Dapper; + +namespace Lion.AbpPro.EntityFrameworkCore.Mysql; [DependsOn(typeof(AbpEntityFrameworkCoreMySQLModule))] +[DependsOn(typeof(AbpDapperModule))] public class AbpProEntityFrameworkCoreMysqlModule : AbpModule { } \ No newline at end of file diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/EfCoreBulkOperationProvider.cs b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/EfCoreBulkOperationProvider.cs index 4f340502..a41c02b6 100644 --- a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/EfCoreBulkOperationProvider.cs +++ b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/Lion/AbpPro/EntityFrameworkCore/Mysql/EfCoreBulkOperationProvider.cs @@ -1,9 +1,18 @@ -using Volo.Abp.Auditing; +using Microsoft.Extensions.Logging; +using MySql.Data.MySqlClient; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Repositories.Dapper; namespace Lion.AbpPro.EntityFrameworkCore.Mysql; public class EfCoreBulkOperationProvider : IEfCoreBulkOperationProvider, ITransientDependency { + private readonly ILogger _logger; + public EfCoreBulkOperationProvider(ILogger logger) + { + _logger = logger; + } + /// /// 批量新增 /// @@ -24,7 +33,9 @@ public class EfCoreBulkOperationProvider : IEfCoreBulkOperationProvider, ITransi { var dbContext = await repository.GetDbContextAsync(); var dbTransaction = dbContext.Database.CurrentTransaction?.GetDbTransaction(); - await dbContext.BulkInsertAsync(entities, dbTransaction as MySqlTransaction, cancellationToken); + var dbConnection = dbContext.Database.GetDbConnection(); + var count = await dbConnection.BulkInsertAsync(dbContext, entities, dbTransaction as MySqlTransaction); + _logger.LogInformation($"批量新增{count}条数据成功"); if (autoSave) { await dbContext.SaveChangesAsync(cancellationToken); diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/EfDapperBulkExtensions.cs b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/EfDapperBulkExtensions.cs new file mode 100644 index 00000000..239d1644 --- /dev/null +++ b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/EfDapperBulkExtensions.cs @@ -0,0 +1,196 @@ + +using System.Data; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Metadata; +using MySql.Data.MySqlClient; + +public static class EfDapperBulkExtensions +{ + /// + /// 使用 EF Core metadata 确保表名、字段名和顺序一致性进行批量插入 + /// + public static async Task BulkInsertAsync( + this IDbConnection connection, + DbContext dbContext, + IEnumerable entities, + IDbTransaction transaction = null, + CancellationToken cancellationToken = default) where T : class + { + // 获取 EF Core 实体类型信息 + var entityType = dbContext.Model.FindEntityType(typeof(T)); + if (entityType == null) + throw new InvalidOperationException($"Entity type {typeof(T).Name} not found in DbContext model."); + + // 获取表名 + var tableName = entityType.GetTableName(); + var schemaName = entityType.GetSchema(); + if (string.IsNullOrEmpty(tableName)) + throw new InvalidOperationException($"Table name not found for entity {typeof(T).Name}."); + + // 获取属性映射信息,确保顺序一致性 + var properties = GetPropertiesInOrder(entityType); + + // 创建 DataTable + var dataTable = CreateDataTable(entities.ToList(), properties, tableName, schemaName); + + // 执行批量插入 + return await BulkInsertDataTableAsync(connection, dataTable, cancellationToken); + } + + /// + /// 创建 DataTable 用于批量插入 + /// + private static DataTable CreateDataTable(List entities, List properties, string tableName, string schemaName) where T : class + { + var dataTable = new DataTable(); + dataTable.TableName = string.IsNullOrEmpty(schemaName) ? tableName : $"{schemaName}.{tableName}"; + + // 添加列,按照 EF Core 中定义的顺序 + foreach (var property in properties) + { + var columnType = GetDbType(property.Property.ClrType); + var column = new DataColumn(property.ColumnName, columnType); + dataTable.Columns.Add(column); + } + + // 添加行 + foreach (var entity in entities) + { + var row = dataTable.NewRow(); + foreach (var property in properties) + { + var value = property.PropertyInfo.GetValue(entity) ?? DBNull.Value; + row[property.ColumnName] = value; + } + dataTable.Rows.Add(row); + } + + return dataTable; + } + + /// + /// 将 DataTable 批量插入到 MySQL + /// + private static async Task BulkInsertDataTableAsync(IDbConnection connection, DataTable dataTable, CancellationToken cancellationToken) + { + var mySqlConnection = connection as MySqlConnection; + if (mySqlConnection == null) + throw new InvalidOperationException("Connection must be MySqlConnection for bulk insert"); + + if (dataTable.Rows.Count == 0) + return 0; + + // 确保连接打开 + if (mySqlConnection.State != ConnectionState.Open) + { + await mySqlConnection.OpenAsync(cancellationToken); + } + + // 使用 MySqlBulkLoader 进行批量插入 + var bulkLoader = new MySqlBulkLoader(mySqlConnection) + { + TableName = dataTable.TableName, + FieldTerminator = "\t", + LineTerminator = "\n", + NumberOfLinesToSkip = 0 + }; + + // 添加字段映射,确保与 DataTable 列顺序一致 + foreach (DataColumn column in dataTable.Columns) + { + bulkLoader.Columns.Add(column.ColumnName); + } + + // 写入数据到临时文件 + var tempFilePath = Path.GetTempFileName(); + try + { + using (var writer = new StreamWriter(tempFilePath, false, System.Text.Encoding.UTF8)) + { + foreach (DataRow row in dataTable.Rows) + { + var values = new object[dataTable.Columns.Count]; + row.ItemArray.CopyTo(values, 0); + var line = string.Join("\t", values.Select(v => v?.ToString()?.Replace("\t", "\\t").Replace("\n", "\\n") ?? "\\N")); + await writer.WriteLineAsync(line); + } + } + + bulkLoader.FileName = tempFilePath; + var result = await bulkLoader.LoadAsync(); + + return result; + } + finally + { + // 清理临时文件 + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + /// + /// 获取属性信息,确保与 EF Core 中定义的顺序一致 + /// + private static List GetPropertiesInOrder(IEntityType entityType) where T : class + { + var properties = new List(); + + // 按 EF Core 中属性的顺序获取 + foreach (var property in entityType.GetProperties()) + { + // 跳过 Shadow Properties(影子属性) + if (property.IsShadowProperty()) + continue; + + var propertyInfo = typeof(T).GetProperty(property.Name, BindingFlags.Instance | BindingFlags.Public); + if (propertyInfo != null && propertyInfo.CanRead && propertyInfo.CanWrite) + { + properties.Add(new PropertyMappingInfo + { + PropertyInfo = propertyInfo, + ColumnName = property.GetColumnName(), + Property = property + }); + } + } + + return properties; + } + + /// + /// 将 .NET 类型转换为 DbType + /// + private static Type GetDbType(Type clrType) + { + // 处理可空类型 + if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + clrType = Nullable.GetUnderlyingType(clrType); + } + + return clrType switch + { + Type t when t == typeof(string) => typeof(string), + Type t when t == typeof(int) => typeof(int), + Type t when t == typeof(long) => typeof(long), + Type t when t == typeof(decimal) => typeof(decimal), + Type t when t == typeof(double) => typeof(double), + Type t when t == typeof(float) => typeof(float), + Type t when t == typeof(bool) => typeof(bool), + Type t when t == typeof(DateTime) => typeof(DateTime), + Type t when t == typeof(Guid) => typeof(Guid), + Type t when t == typeof(byte[]) => typeof(byte[]), + _ => typeof(object) + }; + } + + private class PropertyMappingInfo + { + public PropertyInfo PropertyInfo { get; set; } + public string ColumnName { get; set; } + public IProperty Property { get; set; } + } +} \ No newline at end of file diff --git a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/MySQLBulkInsertExtensions.cs b/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/MySQLBulkInsertExtensions.cs deleted file mode 100644 index 89880336..00000000 --- a/aspnet-core/frameworks/src/Lion.AbpPro.EntityFrameworkCore.Mysql/System/Linq/MySQLBulkInsertExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace System.Linq -{ - public static class MySQLBulkInsertExtensions - { - public static async Task BulkInsertAsync(this DbContext dbCtx, IEnumerable items, MySqlTransaction transaction = null, CancellationToken cancellationToken = default) where TEntity : class - { - var conn = dbCtx.Database.GetDbConnection(); - await conn.OpenIfNeededAsync(cancellationToken); - var dataTable = BulkInsertUtils.BuildDataTable(dbCtx, dbCtx.Set(), items); - var bulkCopy = BuildSqlBulkCopy((MySqlConnection)conn, dbCtx, transaction); - await bulkCopy.WriteToServerAsync(dataTable, cancellationToken); - } - - public static void BulkInsert(this DbContext dbCtx, IEnumerable items, MySqlTransaction transaction = null, CancellationToken cancellationToken = default) where TEntity : class - { - var conn = dbCtx.Database.GetDbConnection(); - conn.OpenIfNeeded(); - var dataTable = BulkInsertUtils.BuildDataTable(dbCtx, dbCtx.Set(), items); - var bulkCopy = BuildSqlBulkCopy((MySqlConnection)conn, dbCtx, transaction); - bulkCopy.WriteToServer(dataTable); - } - - private static MySqlBulkCopy BuildSqlBulkCopy(MySqlConnection conn, DbContext dbCtx, MySqlTransaction transaction = null) where TEntity : class - { - var dbSet = dbCtx.Set(); - var entityType = dbSet.EntityType; - var dbProps = BulkInsertUtils.ParseDbProps(dbCtx, entityType); - - var bulkCopy = new MySqlBulkCopy(conn, transaction) - { - DestinationTableName = entityType.GetTableName() //Schema is not supported by MySQL - }; - - var sourceOrdinal = 0; - foreach (var dbProp in dbProps) - { - var columnName = dbProp.ColumnName; - bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(sourceOrdinal, columnName)); - sourceOrdinal++; - } - - return bulkCopy; - } - } -} \ No newline at end of file diff --git a/aspnet-core/modules/DataDictionaryManagement/src/Lion.AbpPro.DataDictionaryManagement.Domain/DataDictionaries/DataDictionaryManager.cs b/aspnet-core/modules/DataDictionaryManagement/src/Lion.AbpPro.DataDictionaryManagement.Domain/DataDictionaries/DataDictionaryManager.cs index 7e38195b..ca717c3a 100644 --- a/aspnet-core/modules/DataDictionaryManagement/src/Lion.AbpPro.DataDictionaryManagement.Domain/DataDictionaries/DataDictionaryManager.cs +++ b/aspnet-core/modules/DataDictionaryManagement/src/Lion.AbpPro.DataDictionaryManagement.Domain/DataDictionaries/DataDictionaryManager.cs @@ -203,17 +203,19 @@ namespace Lion.AbpPro.DataDictionaryManagement.DataDictionaries /// public virtual async Task DeleteDataDictionaryTypeAsync(Guid id) { - var entity = await _dataDictionaryRepository.FindByIdAsync(id); - if (entity == null) - throw new DataDictionaryDomainException(DataDictionaryManagementErrorCodes.DataDictionaryNotExist); - var detail = entity.Details.FirstOrDefault(e => e.DataDictionaryId == id); - if (detail != null) - { - entity.Details.Remove(detail); - await _dataDictionaryRepository.UpdateAsync(entity); - } - - await _dataDictionaryRepository.DeleteAsync(id); + var entity = new DataDictionary(GuidGenerator.Create(),GuidGenerator.Create().ToString(),GuidGenerator.Create().ToString(),GuidGenerator.Create().ToString(),CurrentTenant.Id); + await _dataDictionaryRepository.InsertManyAsync(new List(){entity}); + // var entity = await _dataDictionaryRepository.FindByIdAsync(id); + // if (entity == null) + // throw new DataDictionaryDomainException(DataDictionaryManagementErrorCodes.DataDictionaryNotExist); + // var detail = entity.Details.FirstOrDefault(e => e.DataDictionaryId == id); + // if (detail != null) + // { + // entity.Details.Remove(detail); + // await _dataDictionaryRepository.UpdateAsync(entity); + // } + // + // await _dataDictionaryRepository.DeleteAsync(id); } } } \ No newline at end of file diff --git a/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/AbpProHttpApiHostModule.cs b/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/AbpProHttpApiHostModule.cs index c92bd3c9..7f88f40d 100644 --- a/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/AbpProHttpApiHostModule.cs +++ b/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/AbpProHttpApiHostModule.cs @@ -1,3 +1,5 @@ +using Lion.AbpPro.EntityFrameworkCore.Mysql; + namespace Lion.AbpPro; [DependsOn( @@ -16,7 +18,8 @@ namespace Lion.AbpPro; typeof(AbpDistributedLockingModule), typeof(AbpBlobStoringFileSystemModule), typeof(AbpProStarterModule), - typeof(AbpSwashbuckleModule) + typeof(AbpSwashbuckleModule), + typeof(AbpProEntityFrameworkCoreMysqlModule) //typeof(AbpBackgroundJobsHangfireModule) )] public partial class AbpProHttpApiHostModule : AbpModule diff --git a/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Lion.AbpPro.HttpApi.Host.csproj b/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Lion.AbpPro.HttpApi.Host.csproj index fb5228ce..edc27d07 100644 --- a/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Lion.AbpPro.HttpApi.Host.csproj +++ b/aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Lion.AbpPro.HttpApi.Host.csproj @@ -32,6 +32,7 @@ +