From ec28eb6c16c4ea51882479812784d3e14cf147ca Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 26 Jul 2026 16:24:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E7=B4=A2=E5=BC=95=E7=BC=A9=E7=9F=AD?= =?UTF-8?q?=E4=B8=BA=20IX=5FCE=5FOffering=5FStatus=5FWaitlistedAt=E3=80=82?= =?UTF-8?q?=20=E5=B0=86=E5=80=99=E8=A1=A5=E8=BF=81=E7=A7=BB=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=AF=E9=87=8D=E5=85=A5=EF=BC=9A=E5=85=88=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=88=97=E5=92=8C=E7=B4=A2=E5=BC=95=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=EF=BC=8C=E5=86=8D=E6=89=A7=E8=A1=8C=20DDL?= =?UTF-8?q?=E3=80=82=20=E9=99=84=E4=BB=B6=E4=B8=AD=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E6=B7=BB=E5=8A=A0=20WaitlistedAt=E3=80=81?= =?UTF-8?q?=E4=BD=86=E7=B4=A2=E5=BC=95=E5=88=9B=E5=BB=BA=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E5=8F=AF=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E9=87=8D=E8=AF=95=EF=BC=8C=E4=B8=8D=E4=BC=9A=E6=8A=A5=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=97=E3=80=82=20=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E5=92=8C=20dotnet=20ef=20=E8=BF=9E=E6=8E=A5=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=BC=80=E5=90=AF=20AllowUserVariables=EF=BC=9BConnector/NET?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD=E8=AF=A5=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E3=80=82Connector/NET=20=E5=AE=98=E6=96=B9=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=20=E5=A2=9E=E5=8A=A0=E6=89=80=E6=9C=89=20MySQL=20?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E5=90=8D=E4=B8=8D=E5=BE=97=E8=B6=85=E8=BF=87?= =?UTF-8?q?=2064=20=E5=AD=97=E7=AC=A6=E7=9A=84=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Persistence/AppDbContext.cs | 2 +- .../Persistence/AppDbContextFactory.cs | 2 +- .../Persistence/DevelopmentSqliteMigrator.cs | 2 +- .../20260726170000_CourseSelectionWaitlist.cs | 95 ++++++++++++++++--- ...000_CourseSelectionGradeLimits.Designer.cs | 3 +- .../MySql/AppDbContextModelSnapshot.cs | 3 +- .../MySqlConnectionStringPolicy.cs | 15 +++ src/Jiaowu.Api/Program.cs | 18 ++-- tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs | 38 +++++++- 9 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index 136e721..a62f567 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -518,7 +518,7 @@ public sealed class AppDbContext(DbContextOptions options) x.CourseSelectionOfferingId, x.Status, x.WaitlistedAt - }); + }).HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); entity.HasOne(x => x.CourseSelectionOffering) .WithMany(x => x.Enrollments) .HasForeignKey(x => x.CourseSelectionOfferingId) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs index 3c3a423..f130f8a 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContextFactory.cs @@ -12,7 +12,7 @@ public sealed class AppDbContextFactory : IDesignTimeDbContextFactory() - .UseMySQL(connectionString) + .UseMySQL(MySqlConnectionStringPolicy.ForApplication(connectionString)) .Options; return new AppDbContext(options); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs index df4200c..56ce0d0 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/DevelopmentSqliteMigrator.cs @@ -1611,7 +1611,7 @@ public sealed class DevelopmentSqliteMigrator( ADD COLUMN "WaitlistedAt" TEXT NULL; """, """ - CREATE INDEX IF NOT EXISTS "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt" + CREATE INDEX IF NOT EXISTS "IX_CE_Offering_Status_WaitlistedAt" ON "CourseEnrollments" ("CourseSelectionOfferingId", "Status", "WaitlistedAt"); """ ]; diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs index 448cf49..a410c5b 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726170000_CourseSelectionWaitlist.cs @@ -12,26 +12,91 @@ public partial class CourseSelectionWaitlist : Migration { protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "WaitlistedAt", - table: "CourseEnrollments", - type: "datetime(6)", - nullable: true); + migrationBuilder.Sql(""" + SET @waitlisted_column_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `COLUMN_NAME` = 'WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @add_waitlisted_column_sql = IF( + @waitlisted_column_exists = 0, + 'ALTER TABLE `CourseEnrollments` ADD COLUMN `WaitlistedAt` datetime(6) NULL', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE add_waitlisted_column_stmt FROM @add_waitlisted_column_sql;"); + migrationBuilder.Sql("EXECUTE add_waitlisted_column_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE add_waitlisted_column_stmt;"); - migrationBuilder.CreateIndex( - name: "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", - table: "CourseEnrollments", - columns: new[] { "CourseSelectionOfferingId", "Status", "WaitlistedAt" }); + migrationBuilder.Sql(""" + SET @waitlist_index_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`STATISTICS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `INDEX_NAME` = 'IX_CE_Offering_Status_WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @add_waitlist_index_sql = IF( + @waitlist_index_exists = 0, + 'CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt` ON `CourseEnrollments` (`CourseSelectionOfferingId`, `Status`, `WaitlistedAt`)', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE add_waitlist_index_stmt FROM @add_waitlist_index_sql;"); + migrationBuilder.Sql("EXECUTE add_waitlist_index_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE add_waitlist_index_stmt;"); } protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropIndex( - name: "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", - table: "CourseEnrollments"); + migrationBuilder.Sql(""" + SET @waitlist_index_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`STATISTICS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `INDEX_NAME` = 'IX_CE_Offering_Status_WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @drop_waitlist_index_sql = IF( + @waitlist_index_exists > 0, + 'DROP INDEX `IX_CE_Offering_Status_WaitlistedAt` ON `CourseEnrollments`', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE drop_waitlist_index_stmt FROM @drop_waitlist_index_sql;"); + migrationBuilder.Sql("EXECUTE drop_waitlist_index_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE drop_waitlist_index_stmt;"); - migrationBuilder.DropColumn( - name: "WaitlistedAt", - table: "CourseEnrollments"); + migrationBuilder.Sql(""" + SET @waitlisted_column_exists = ( + SELECT COUNT(*) + FROM `INFORMATION_SCHEMA`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() + AND `TABLE_NAME` = 'CourseEnrollments' + AND `COLUMN_NAME` = 'WaitlistedAt' + ); + """); + migrationBuilder.Sql(""" + SET @drop_waitlisted_column_sql = IF( + @waitlisted_column_exists > 0, + 'ALTER TABLE `CourseEnrollments` DROP COLUMN `WaitlistedAt`', + 'SELECT 1' + ); + """); + migrationBuilder.Sql( + "PREPARE drop_waitlisted_column_stmt FROM @drop_waitlisted_column_sql;"); + migrationBuilder.Sql("EXECUTE drop_waitlisted_column_stmt;"); + migrationBuilder.Sql("DEALLOCATE PREPARE drop_waitlisted_column_stmt;"); } } diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs index babd87d..24602b0 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/20260726180000_CourseSelectionGradeLimits.Designer.cs @@ -748,7 +748,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("StudentId", "Status"); - b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt"); + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); b.ToTable("CourseEnrollments"); }); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs index 82c4ac9..5f013d3 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/Migrations/MySql/AppDbContextModelSnapshot.cs @@ -745,7 +745,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql b.HasIndex("StudentId", "Status"); - b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt"); + b.HasIndex("CourseSelectionOfferingId", "Status", "WaitlistedAt") + .HasDatabaseName("IX_CE_Offering_Status_WaitlistedAt"); b.ToTable("CourseEnrollments"); }); diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs b/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs new file mode 100644 index 0000000..71ff212 --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Persistence/MySqlConnectionStringPolicy.cs @@ -0,0 +1,15 @@ +using MySql.Data.MySqlClient; + +namespace Jiaowu.Api.Infrastructure.Persistence; + +public static class MySqlConnectionStringPolicy +{ + public static string ForApplication(string connectionString) + { + var builder = new MySqlConnectionStringBuilder(connectionString) + { + AllowUserVariables = true + }; + return builder.ConnectionString; + } +} diff --git a/src/Jiaowu.Api/Program.cs b/src/Jiaowu.Api/Program.cs index 94a07a9..008a727 100644 --- a/src/Jiaowu.Api/Program.cs +++ b/src/Jiaowu.Api/Program.cs @@ -141,14 +141,16 @@ builder.Services.AddDbContextPool(options => throw new InvalidOperationException( "缺少 MySQL 连接串。请通过 ConnectionStrings__MySql 环境变量配置。"); } - options.UseMySQL(connectionString, mySqlOptions => - { - mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds); - mySqlOptions.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(10), - errorNumbersToAdd: null); - }); + options.UseMySQL( + MySqlConnectionStringPolicy.ForApplication(connectionString), + mySqlOptions => + { + mySqlOptions.CommandTimeout(databaseOptions.CommandTimeoutSeconds); + mySqlOptions.EnableRetryOnFailure( + maxRetryCount: 5, + maxRetryDelay: TimeSpan.FromSeconds(10), + errorNumbersToAdd: null); + }); }); var redisConnectionString = builder.Configuration.GetConnectionString("Redis"); diff --git a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs index 7af6629..683e9be 100644 --- a/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs +++ b/tests/Jiaowu.Api.Tests/MySqlMigrationTests.cs @@ -2,6 +2,7 @@ using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using MySql.Data.MySqlClient; namespace Jiaowu.Api.Tests; @@ -27,9 +28,14 @@ public sealed class MySqlMigrationTests Assert.Contains("CREATE TABLE `WarningRules`", script); Assert.Contains("ADD `CheckInMethod` int NOT NULL DEFAULT 1", script); Assert.Contains("CREATE UNIQUE INDEX `IX_AttendanceSheets_CheckInToken`", script); - Assert.Contains("ADD `WaitlistedAt` datetime(6) NULL", script); + Assert.Contains("ADD COLUMN `WaitlistedAt` datetime(6) NULL", script); Assert.Contains( - "CREATE INDEX `IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt`", + "CREATE INDEX `IX_CE_Offering_Status_WaitlistedAt`", + script); + Assert.Contains("@waitlisted_column_exists", script); + Assert.Contains("@waitlist_index_exists", script); + Assert.DoesNotContain( + "IX_CourseEnrollments_CourseSelectionOfferingId_Status_WaitlistedAt", script); Assert.Contains( "ADD `MaxCourseCount` int NULL", @@ -86,6 +92,34 @@ public sealed class MySqlMigrationTests Assert.Contains(ids[1].ToString(), sql, StringComparison.OrdinalIgnoreCase); } + [Fact] + public void MySql_index_names_fit_the_server_identifier_limit() + { + using var db = new AppDbContext(CreateMySqlOptions()); + + var indexNames = db.Model.GetEntityTypes() + .SelectMany(x => x.GetIndexes()) + .Select(x => x.GetDatabaseName()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + Assert.NotEmpty(indexNames); + Assert.All(indexNames, name => Assert.True( + name!.Length <= 64, + $"MySQL index name '{name}' has {name.Length} characters.")); + } + + [Fact] + public void Application_mysql_connections_allow_migration_user_variables() + { + var normalized = MySqlConnectionStringPolicy.ForApplication( + "Server=localhost;Database=jiaowu;User=test;Password=test;"); + + var builder = new MySqlConnectionStringBuilder(normalized); + + Assert.True(builder.AllowUserVariables); + } + private static DbContextOptions CreateMySqlOptions() => new DbContextOptionsBuilder() .UseMySQL(