package com.mydreamjob.integration; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.MigrationVersion; import org.flywaydb.core.api.output.MigrateResult; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.MySQLContainer; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * V57 展示字段在**真实 MySQL** 上的迁移验证(隔离容器,容器/库名带 b01 前缀)。 * *
逐项覆盖:数据库版本记录;从 V56 基线升级到 V57;旧数据保留、新列为 NULL; * 新字段写入与重新查询;更新清空语义(NULL);重复迁移无待执行项且 V57 只有一条历史; * 空库(独立容器)从零完整迁移到 V57。
* *连接统一追加 {@code characterEncoding=UTF-8},避免 MySQL 5.7 服务器默认字符集导致的种子中文写入失败。 * Docker 不可用时环境性跳过(不宣称通过)。
*/ class JobCompanyDisplayFieldsMysqlMigrationTest { private static final String MIG = "classpath:db/migration"; @Test void mysql57StagedUpgradeAndEmptyDatabase() throws Exception { verifyMigration(System.getProperty("b01.mysql.image57", "mysql:5.7.36"), "57"); } @Test void mysql8StagedUpgradeAndEmptyDatabase() throws Exception { verifyMigration(System.getProperty("b01.mysql.image8", "mysql:8.4"), "8"); } private void verifyMigration(String image, String tag) throws Exception { assumeTrue(dockerAvailable(), "Docker 不可用,跳过真实 MySQL 迁移验证"); String version; // ── 阶段一:V56 基线 → 旧数据 → 升级 V57 ────────────────── try (MySQLContainer> mysql = new MySQLContainer<>(image) .withDatabaseName("mydreamjob_b01_" + tag) .withUsername("b01") .withPassword("b01-test-only")) { mysql.start(); String url = charset(mysql.getJdbcUrl()); String user = mysql.getUsername(); String pass = mysql.getPassword(); // MySQL 5.7 服务器默认字符集为 latin1;与真实部署一致,先把库默认字符集设为 utf8mb4, // 使未显式指定字符集的迁移建表(如 V37)可写入中文种子数据。 ensureUtf8mb4(url, user, pass, mysql.getDatabaseName()); version = scalar(url, user, pass, "SELECT VERSION()"); System.out.println("[B01][" + image + "] SELECT VERSION() = " + version); assertNotNull(version); // 1) 只迁移到 V56 基线 MigrateResult to56 = flyway(url, user, pass, "56").migrate(); assertTrue(to56.success, "迁移到 V56 应成功"); assertEquals(0, count(url, user, pass, "SELECT count(*) FROM flyway_schema_history WHERE version = '57'"), "V56 基线不应含 V57"); // 2) 旧企业与旧岗位数据 try (Connection c = DriverManager.getConnection(url, user, pass); Statement st = c.createStatement()) { st.executeUpdate("INSERT INTO users(phone, role, is_active, is_verified) " + "VALUES('19900009001','COMPANY',1,1)"); st.executeUpdate("INSERT INTO companies(owner_user_id, name, description, industry, verified) " + "SELECT id,'B01旧企业','旧简介','旧行业',1 FROM users WHERE phone='19900009001'"); st.executeUpdate("INSERT INTO jobs(company_id, title, description, requirements, city, " + "employment_type, salary_min, salary_max, status) SELECT c.id,'B01旧岗位','旧描述','旧要求'," + "'北京','FULL_TIME',10000,20000,'PUBLISHED' FROM companies c WHERE c.name='B01旧企业'"); } // 3) 升级到 V57 MigrateResult latest = flyway(url, user, pass, "57").migrate(); assertTrue(latest.success, "升级到 V57 应成功"); assertEquals(1, latest.migrationsExecuted, "从 V56 升级应只执行一条迁移(V57)"); // 4) 旧数据保留、新列为 NULL assertEquals(1, count(url, user, pass, "SELECT count(*) FROM companies WHERE name='B01旧企业'")); assertEquals(1, count(url, user, pass, "SELECT count(*) FROM jobs WHERE title='B01旧岗位'")); assertNull(scalar(url, user, pass, "SELECT company_size FROM companies WHERE name='B01旧企业'")); assertNull(scalar(url, user, pass, "SELECT experience FROM jobs WHERE title='B01旧岗位'")); assertNull(scalar(url, user, pass, "SELECT benefits_json FROM jobs WHERE title='B01旧岗位'")); assertNull(scalar(url, user, pass, "SELECT salary_months FROM jobs WHERE title='B01旧岗位'")); assertNull(scalar(url, user, pass, "SELECT work_location FROM jobs WHERE title='B01旧岗位'")); // 5) 新增字段写入 + 重新查询 + 更新清空语义(NULL) try (Connection c = DriverManager.getConnection(url, user, pass); Statement st = c.createStatement()) { st.executeUpdate("UPDATE companies SET company_size='100-499人' WHERE name='B01旧企业'"); st.executeUpdate("UPDATE jobs SET experience='3-5年', education='本科', " + "benefits_json='[\"五险一金\",\"弹性工作\"]', salary_months=14, work_location='上海市浦东新区' " + "WHERE title='B01旧岗位'"); } assertEquals("100-499人", scalar(url, user, pass, "SELECT company_size FROM companies WHERE name='B01旧企业'")); assertEquals("[\"五险一金\",\"弹性工作\"]", scalar(url, user, pass, "SELECT benefits_json FROM jobs WHERE title='B01旧岗位'")); assertEquals("14", scalar(url, user, pass, "SELECT salary_months FROM jobs WHERE title='B01旧岗位'")); try (Connection c = DriverManager.getConnection(url, user, pass); Statement st = c.createStatement()) { st.executeUpdate("UPDATE companies SET company_size=NULL WHERE name='B01旧企业'"); st.executeUpdate("UPDATE jobs SET benefits_json=NULL, salary_months=NULL WHERE title='B01旧岗位'"); } assertNull(scalar(url, user, pass, "SELECT company_size FROM companies WHERE name='B01旧企业'")); assertNull(scalar(url, user, pass, "SELECT benefits_json FROM jobs WHERE title='B01旧岗位'")); assertNull(scalar(url, user, pass, "SELECT salary_months FROM jobs WHERE title='B01旧岗位'")); // 6) 重复迁移无待执行项;V57 只有一条历史且成功;新增列不重复 MigrateResult again = flyway(url, user, pass, "57").migrate(); assertEquals(0, again.migrationsExecuted, "再次迁移应无待执行项"); assertEquals(1, count(url, user, pass, "SELECT count(*) FROM flyway_schema_history WHERE version='57' AND success=1"), "flyway_schema_history 中 V57 应只有一条"); assertEquals(6, count(url, user, pass, newColumnCountSql()), "新增 6 列应恰好各一列,不重复增加"); System.out.println("[B01][" + image + "] staged upgrade verified: version=" + version + ", to56=" + to56.migrationsExecuted + ", upgrade=" + latest.migrationsExecuted + ", rerun=" + again.migrationsExecuted); } // ── 阶段二:空库(独立容器)从零完整迁移到 V57 ──────────── try (MySQLContainer> empty = new MySQLContainer<>(image) .withDatabaseName("mydreamjob_b01_empty_" + tag) .withUsername("b01") .withPassword("b01-test-only")) { empty.start(); String url = charset(empty.getJdbcUrl()); String user = empty.getUsername(); String pass = empty.getPassword(); ensureUtf8mb4(url, user, pass, empty.getDatabaseName()); MigrateResult fromScratch = flyway(url, user, pass, "57").migrate(); assertTrue(fromScratch.success, "空库应能完整迁移到 V57"); assertEquals("57", scalar(url, user, pass, "SELECT version FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 1"), "空库迁移后的最高版本应为 57"); assertEquals(6, count(url, user, pass, newColumnCountSql()), "空库迁移后新增 6 列存在"); System.out.println("[B01][" + image + "] empty-db migration verified: executed=" + fromScratch.migrationsExecuted); } } private static String newColumnCountSql() { return "SELECT count(*) FROM information_schema.columns " + "WHERE table_schema = DATABASE() AND ((table_name='companies' AND column_name='company_size') " + "OR (table_name='jobs' AND column_name IN ('experience','education','benefits_json'," + "'salary_months','work_location')))"; } private static String charset(String url) { return url + (url.contains("?") ? "&" : "?") + "useUnicode=true&characterEncoding=UTF-8"; } private static void ensureUtf8mb4(String url, String user, String pass, String database) throws Exception { try (Connection c = DriverManager.getConnection(url, user, pass); Statement st = c.createStatement()) { st.executeUpdate("ALTER DATABASE `" + database + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); } } private Flyway flyway(String url, String user, String pass, String target) { var cfg = Flyway.configure().dataSource(url, user, pass).locations(MIG); // 与生产启动路径一致:应用 MySQL 5.7 兼容钩子(仅替换 V49;MySQL 8/H2 下为空操作)。 com.mydreamjob.config.MySql57MigrationConfiguration.configure(cfg); if (target != null) { cfg = cfg.target(MigrationVersion.fromVersion(target)); } return cfg.load(); } private String scalar(String url, String user, String pass, String sql) throws Exception { try (Connection c = DriverManager.getConnection(url, user, pass); Statement st = c.createStatement(); ResultSet rs = st.executeQuery(sql)) { return rs.next() ? rs.getString(1) : null; } } private int count(String url, String user, String pass, String sql) throws Exception { return Integer.parseInt(scalar(url, user, pass, sql)); } private boolean dockerAvailable() { try { return DockerClientFactory.instance().isDockerAvailable(); } catch (Throwable ignored) { return false; } } }