| New file |
| | |
| | | import java.sql.*; |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * 把旧开发库(默认 127.0.0.1:6134/talent_card)的业务数据导入项目本地库(默认 127.0.0.1:8134/talent_card)。 |
| | | * |
| | | * <p>背景:8134 这个库的表在 09-13 被按最新迁移重建过,原先导入的那份数据没了;6134 上保存着 |
| | | * 09-11 从远程开发库导出的完整快照,两份库的 91 张业务表一一对应,因此可以按列交集回灌。 |
| | | * |
| | | * <p>三条安全约束: |
| | | * <ul> |
| | | * <li>只做 INSERT IGNORE:主键/唯一键冲突的行保持本地现状,支付验证的种子数据(进件、结算卡、商品)不被覆盖;</li> |
| | | * <li>只导入两张库都存在的表,列取交集,旧库多出来的列直接忽略;</li> |
| | | * <li>纯新增,不执行任何 DELETE / UPDATE / DROP。</li> |
| | | * </ul> |
| | | * |
| | | * <p>用法:java RestoreOldData [--dry-run] |
| | | */ |
| | | public class RestoreOldData { |
| | | |
| | | private static final String SCHEMA = "talent_card"; |
| | | private static final String OLD_HOST = System.getenv().getOrDefault("OLD_DB_HOST", "127.0.0.1:6134"); |
| | | private static final String NEW_HOST = System.getenv().getOrDefault("NEW_DB_HOST", "127.0.0.1:8134"); |
| | | private static final String USER = System.getenv().getOrDefault("MYSQL_USERNAME", "root"); |
| | | private static final String PASSWORD = System.getenv().getOrDefault("MYSQL_PASSWORD", "123456"); |
| | | |
| | | /** 参考数据与迁移元数据不参与回灌:前者本地已有,后者结构不同。 */ |
| | | private static final Set<String> SKIP = Set.of("flyway_schema_history", "bank_branch_directory"); |
| | | |
| | | public static void main(String[] args) throws Exception { |
| | | boolean dryRun = Arrays.asList(args).contains("--dry-run"); |
| | | String base = "jdbc:mysql://%s/" + SCHEMA + "?useUnicode=true&characterEncoding=utf8" |
| | | + "&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false" |
| | | + "&rewriteBatchedStatements=true"; |
| | | |
| | | try (Connection old = DriverManager.getConnection(String.format(base, OLD_HOST), USER, PASSWORD); |
| | | Connection fresh = DriverManager.getConnection(String.format(base, NEW_HOST), USER, PASSWORD)) { |
| | | |
| | | List<String> tables = tables(fresh); |
| | | System.out.println("旧库 " + OLD_HOST + " -> 新库 " + NEW_HOST + ",共 " + tables.size() + " 张表" |
| | | + (dryRun ? "(干跑,不写入)" : "")); |
| | | System.out.println(); |
| | | |
| | | int totalRead = 0; |
| | | int totalInserted = 0; |
| | | int totalSkipped = 0; |
| | | List<String> problems = new ArrayList<>(); |
| | | |
| | | try (Statement statement = fresh.createStatement()) { |
| | | statement.execute("set foreign_key_checks = 0"); |
| | | for (String table : tables) { |
| | | if (SKIP.contains(table)) { |
| | | System.out.printf("%-44s 跳过(参考数据/迁移元数据)%n", table); |
| | | continue; |
| | | } |
| | | |
| | | List<String> oldColumns = columns(old, table); |
| | | List<String> newColumns = columns(fresh, table); |
| | | if (oldColumns.isEmpty()) { |
| | | problems.add(table + ":旧库没有这张表"); |
| | | System.out.printf("%-44s 跳过(旧库无此表)%n", table); |
| | | continue; |
| | | } |
| | | |
| | | Set<String> known = new HashSet<>(); |
| | | for (String column : oldColumns) known.add(column.toLowerCase(Locale.ROOT)); |
| | | List<String> shared = new ArrayList<>(); |
| | | for (String column : newColumns) { |
| | | if (known.contains(column.toLowerCase(Locale.ROOT))) shared.add(column); |
| | | } |
| | | if (shared.isEmpty()) { |
| | | problems.add(table + ":没有可共用的列"); |
| | | System.out.printf("%-44s 跳过(无共用列)%n", table); |
| | | continue; |
| | | } |
| | | |
| | | Outcome outcome = dryRun ? readOnly(old, table, shared) : copy(old, fresh, table, shared); |
| | | totalRead += outcome.read; |
| | | totalInserted += outcome.inserted; |
| | | totalSkipped += outcome.skipped; |
| | | if (!outcome.errors.isEmpty()) problems.add(table + ":" + outcome.errors.get(0)); |
| | | |
| | | System.out.printf("%-44s 读出 %-6d 写入 %-6d 跳过 %-6d 列 %d/%d%s%n", |
| | | table, outcome.read, outcome.inserted, outcome.skipped, shared.size(), newColumns.size(), |
| | | outcome.errors.isEmpty() ? "" : " 错误 " + outcome.errors.size() + " 条"); |
| | | } |
| | | statement.execute("set foreign_key_checks = 1"); |
| | | } |
| | | |
| | | System.out.println(); |
| | | System.out.println("================ 汇总 ================"); |
| | | System.out.println("读出 " + totalRead + " 行,写入 " + totalInserted + " 行,跳过(已存在)" + totalSkipped + " 行"); |
| | | if (!problems.isEmpty()) { |
| | | System.out.println("需要注意:" + problems.size() + " 项"); |
| | | for (String problem : problems) System.out.println(" - " + problem); |
| | | } |
| | | System.out.println("====================================="); |
| | | } |
| | | } |
| | | |
| | | private record Outcome(int read, int inserted, int skipped, List<String> errors) { |
| | | } |
| | | |
| | | private static List<String> tables(Connection connection) throws SQLException { |
| | | List<String> tables = new ArrayList<>(); |
| | | try (PreparedStatement statement = connection.prepareStatement( |
| | | "select table_name from information_schema.tables where table_schema = ? and table_type = 'BASE TABLE' order by table_name")) { |
| | | statement.setString(1, SCHEMA); |
| | | try (ResultSet resultSet = statement.executeQuery()) { |
| | | while (resultSet.next()) tables.add(resultSet.getString(1)); |
| | | } |
| | | } |
| | | return tables; |
| | | } |
| | | |
| | | private static List<String> columns(Connection connection, String table) throws SQLException { |
| | | List<String> columns = new ArrayList<>(); |
| | | try (PreparedStatement statement = connection.prepareStatement( |
| | | "select column_name from information_schema.columns where table_schema = ? and table_name = ? order by ordinal_position")) { |
| | | statement.setString(1, SCHEMA); |
| | | statement.setString(2, table); |
| | | try (ResultSet resultSet = statement.executeQuery()) { |
| | | while (resultSet.next()) columns.add(resultSet.getString(1)); |
| | | } |
| | | } |
| | | return columns; |
| | | } |
| | | |
| | | private static Outcome readOnly(Connection old, String table, List<String> shared) throws SQLException { |
| | | String select = "select " + quoted(shared) + " from `" + table + "`"; |
| | | int read = 0; |
| | | try (Statement statement = old.createStatement()) { |
| | | try (ResultSet resultSet = statement.executeQuery(select)) { |
| | | while (resultSet.next()) read++; |
| | | } |
| | | } |
| | | return new Outcome(read, 0, 0, List.of()); |
| | | } |
| | | |
| | | private static Outcome copy(Connection old, Connection fresh, String table, List<String> shared) throws SQLException { |
| | | String select = "select " + quoted(shared) + " from `" + table + "`"; |
| | | String placeholders = String.join(", ", Collections.nCopies(shared.size(), "?")); |
| | | String insert = "insert ignore into `" + table + "` (" + quoted(shared) + ") values (" + placeholders + ")"; |
| | | |
| | | int read = 0; |
| | | int inserted = 0; |
| | | int skipped = 0; |
| | | List<String> errors = new ArrayList<>(); |
| | | |
| | | try (Statement reader = old.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { |
| | | reader.setFetchSize(Integer.MIN_VALUE); |
| | | try (ResultSet resultSet = reader.executeQuery(select); |
| | | PreparedStatement writer = fresh.prepareStatement(insert)) { |
| | | |
| | | while (resultSet.next()) { |
| | | read++; |
| | | for (int index = 1; index <= shared.size(); index++) { |
| | | writer.setObject(index, resultSet.getObject(index)); |
| | | } |
| | | try { |
| | | int affected = writer.executeUpdate(); |
| | | if (affected > 0) inserted++; |
| | | else skipped++; |
| | | } catch (SQLException exception) { |
| | | errors.add("第 " + read + " 行:" + exception.getMessage()); |
| | | if (errors.size() > 20) break; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return new Outcome(read, inserted, skipped, errors); |
| | | } |
| | | |
| | | private static String quoted(List<String> columns) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (String column : columns) { |
| | | if (builder.length() > 0) builder.append(", "); |
| | | builder.append('`').append(column).append('`'); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | } |