Прямой перенос данных между базами: Подробный разбор метода на примере кода java

Салимжанов Р.

Введение

Что такое метод переноса данных?

Это способ копирования данных из одной базы (например, Firebird) в другую (например, Postgres) напрямую, без промежуточных шагов. Давайте разберем его на примере кода.

Основной алгоритм

  1. Подготовка целевой БД (создание таблиц если нужно)
  2. Чтение данных из исходной таблицы
  3. Пакетная запись в целевую БД
  4. Обработка конфликтов (обновление существующих записей)

Пример проблемы

Допустим есть небольшое задание: перенести кучу данных из одной БД в другую. У меня есть две базы:

Прямой перенос данных между базами: Подробный разбор метода на примере кода java

Firebird с двумя заполненными таблицами например:

И PostgreSQL , куда нужно загрузить эти же таблицы:

CREATE TABLE test_table_pg1 ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE test_table_pg1_linked ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, fb1_id INTEGER, linked_name VARCHAR(100) NOT NULL, created_at TIMESTAMP, FOREIGN KEY (fb1_id) REFERENCES test_table_pg1(id) );

Задача: написать программу на Java, которая перенесет гору записей из Firebird в Postgres, не дублируя их и обновляя при необходимости.

Разбор кода шаг за шагом

1. Подготовка БД

private void preparePostgresDatabase() throws SQLException { try (Connection conn = postgresDataSource.getConnection(); Statement stmt = conn.createStatement()) { conn.setAutoCommit(false); // Создаем таблицы если не существуют stmt.executeUpdate(""" CREATE TABLE IF NOT EXISTS test_table_pg1 ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"""); stmt.executeUpdate(""" CREATE TABLE IF NOT EXISTS test_table_pg1_linked ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, fb1_id INTEGER, linked_name VARCHAR(100) NOT NULL, created_at TIMESTAMP, FOREIGN KEY (fb1_id) REFERENCES test_table_pg1(id) )"""); conn.commit(); } }

Что делает: Проверяет существование таблиц в Postgres и создает их при необходимости.

2. Умная вставка (UPSERT)

private String buildUpsertSql(String tableName, String[] columns) { StringBuilder sql = new StringBuilder("INSERT INTO ") .append(tableName) .append(" (") .append(String.join(", ", columns)) .append(") VALUES ("); for (int i = 0; i < columns.length; i++) { sql.append("?"); if (i < columns.length - 1) sql.append(", "); } sql.append(") ON CONFLICT (id) DO UPDATE SET "); boolean first = true; for (String column : columns) { if (!column.equals("id")) { if (!first) sql.append(", "); sql.append(column).append(" = EXCLUDED.").append(column); first = false; } } return sql.toString(); }

1) INSERT INTO

  • Начинает с INSERT INTO tableName (col1, col2, …) и VALUES (?, ?, …), где число ? соответствует числу колонок.

2) ON CONFLICT (id)

  • Добавляет ON CONFLICT (id) — ключевую колонку id берём за определяющую уникальность.

3) DO UPDATE SET …

  • После DO UPDATE SET перечисляет все колонки, кроме id, в виде col = EXCLUDED.col, чтобы при конфликте (то есть если запись с таким id уже есть) обновить существующие поля новыми значениями.

3. Чтение и вставка данных из исходной таблицы

private void transferTable(String fbTableName, String columns) throws SQLException { // 1) Определяем имя целевой таблицы и список колонок String pgTableName = getPgTableName(fbTableName); String[] columnArray = columns.split(","); // 2) Открываем подключения и читаем данные из Firebird try (Connection srcConn = firebirdDataSource.getConnection(); Connection dstConn = postgresDataSource.getConnection(); Statement stmt = srcConn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT " + columns + " FROM " + fbTableName)) { // 3) Отключаем автокоммит в PostgreSQL — будем вручную управлять транзакциями dstConn.setAutoCommit(false); // 4) Строим SQL для вставки или обновления (upsert) String sql = buildUpsertSql(pgTableName, columnArray); // 5) Готовим PreparedStatement на основе этого SQL try (PreparedStatement pstmt = dstConn.prepareStatement(sql)) { int batchSize = 0; // 6) Проходим по каждой строке из исходного ResultSet while (rs.next()) { // 6.1) Заполняем параметры pstmt значениями из Firebird for (int i = 0; i < columnArray.length; i++) { // rs.getObject возвращает значение колонки по имени pstmt.setObject(i + 1, rs.getObject(columnArray[i].trim())); } // 6.2) Добавляем текущую команду в пакет (batch) pstmt.addBatch(); // 6.3) Каждые 1000 строк выполняем пакет и коммит if (++batchSize % 1000 == 0) { pstmt.executeBatch(); // выполняем накопленные INSERT/UPDATE dstConn.commit(); // подтверждаем изменения } } // 7) После цикла отправляем остаток батча и делаем финальный коммит pstmt.executeBatch(); dstConn.commit(); } catch (SQLException e) { // 8) В случае ошибки откатываем все изменения в этой транзакции dstConn.rollback(); throw e; // пробрасываем дальше, чтобы внешний код узнал об ошибке } } }

Объясню по шагам:

  1. getPgTableName(fbTableName) Преобразует имя таблицы из Firebird в имя таблицы в Postgres. В нашем случае просто заменяет fb1 → pg1.
  2. columns.split(",") Из строки вида "id,name,created_at" делает массив ["id", "name", "created_at"].
  3. Try-with-resources Открываются сразу два соединения (srcConn и dstConn), создаётся Statement для чтения и ResultSet с данными. Всё это автоматически закроется в конце блока, даже если упадёт исключение.
  4. Отключение автокоммита По умолчанию каждая операция INSERT сразу фиксируется. Мы отключаем это, чтобы группировать операции в одну транзакцию и контролировать момент коммита.
  5. buildUpsertSql(...) Возвращает строку SQL примерно такого вида:
INSERT INTO test_table_pg1 (id, name, created_at) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, created_at = EXCLUDED.created_at

Это гарантирует, что при повторной вставке записи с тем же id она будет обновлена, а не дублирована.

6 Заполнение и сборка батчей

  • В цикле while (rs.next()) читаем каждую строку из Firebird.
  • С помощью pstmt.setObject(...) последовательно подставляем значения в “?”, соответствующие колонкам.
  • pstmt.addBatch() собирает все команды в один пакет.
  • По достижении 1000 команд делаем executeBatch() (отправляем все эти 1000 запросов на сервер) и commit() (фликтим транзакцию).

7 Финальная запись и коммит После того как строки из ResultSet закончились, обязательно выполняем оставшийся пакет (executeBatch()) и фиксируем его (commit()).

Вывод и обоснование выбора метода прямого переноса данных

Метод прямого переноса данных между базами данных, рассмотренный выше, — это оптимальный способ миграции, когда необходимо:

  • Скопировать большие объёмы данных без создания промежуточных файлов (например, CSV, XML и т.д.),
  • Сохранить структуру и логику связи таблиц (включая внешний ключ),
  • Обновлять уже существующие данные, а не дублировать их при повторном запуске,
  • И работать с двумя различными СУБД (в примере — Firebird и PostgreSQL).

ПОЛНЫЙ КОД

package com.example.db.migrator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import javax.sql.DataSource; import java.sql.*; @Service @RequiredArgsConstructor public class DataTransferService { private final DataSource firebirdDataSource; private final DataSource postgresDataSource; public void transferData() { try { preparePostgresDatabase(); // Подготовка БД transferTable("test_table_fb1", "id,name,created_at"); // Перенос основной таблицы transferTable("test_table_fb1_linked", "id,fb1_id,linked_name,created_at"); // Перенос связанной таблицы System.out.println("✅ Migration completed successfully!"); } catch (Exception e) { System.err.println("❌ Migration failed: " + e.getMessage()); e.printStackTrace(); } } private void transferTable(String fbTableName, String columns) throws SQLException { String pgTableName = getPgTableName(fbTableName); String[] columnArray = columns.split(","); try (Connection srcConn = firebirdDataSource.getConnection(); Connection dstConn = postgresDataSource.getConnection(); Statement stmt = srcConn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT " + columns + " FROM " + fbTableName)) { dstConn.setAutoCommit(false); String sql = buildUpsertSql(pgTableName, columnArray); try (PreparedStatement pstmt = dstConn.prepareStatement(sql)) { int batchSize = 0; while (rs.next()) { for (int i = 0; i < columnArray.length; i++) { pstmt.setObject(i + 1, rs.getObject(columnArray[i].trim())); } pstmt.addBatch(); if (++batchSize % 1000 == 0) { pstmt.executeBatch(); dstConn.commit(); } } pstmt.executeBatch(); dstConn.commit(); } catch (SQLException e) { dstConn.rollback(); throw e; } } } private String buildUpsertSql(String tableName, String[] columns) { StringBuilder sql = new StringBuilder("INSERT INTO ") .append(tableName) .append(" (") .append(String.join(", ", columns)) .append(") VALUES ("); for (int i = 0; i < columns.length; i++) { sql.append("?"); if (i < columns.length - 1) sql.append(", "); } sql.append(") ON CONFLICT (id) DO UPDATE SET "); boolean first = true; for (String column : columns) { if (!column.equals("id")) { if (!first) sql.append(", "); sql.append(column).append(" = EXCLUDED.").append(column); first = false; } } return sql.toString(); } private String getPgTableName(String fbTableName) { return fbTableName.replace("fb1", "pg1"); } private void preparePostgresDatabase() throws SQLException { try (Connection conn = postgresDataSource.getConnection(); Statement stmt = conn.createStatement()) { conn.setAutoCommit(false); // Создаем таблицы если не существуют stmt.executeUpdate(""" CREATE TABLE IF NOT EXISTS test_table_pg1 ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"""); stmt.executeUpdate(""" CREATE TABLE IF NOT EXISTS test_table_pg1_linked ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, fb1_id INTEGER, linked_name VARCHAR(100) NOT NULL, created_at TIMESTAMP, FOREIGN KEY (fb1_id) REFERENCES test_table_pg1(id) )"""); conn.commit(); } } }

PS

Конечно же, эта статья дает только суть. Основная структура будет намного сложнее. Например если рассмотреть с точки зрения информационной безопасности, можно выделить кучу рисков. Ну например:

1. Защита учетных данных Хранение их в открытом виде в конфигах — прямая угроза.

2. Защита данных в транзите Данные передаются в открытом виде между приложением и СУБД. Соответственно следует использовать SSL/TLS соединения.

3. Предотвращение SQL-инъекций Динамическое формирование SQL-запросов с именами таблиц через конкатенацию строк лучше не использовать. А например, сделать валидацию имен таблиц через белый список.

4. Принцип минимальных привилегий Приложение работает с правами суперпользователя БД. В безопасности это вообще “атата”.

И так далее и далее. Все перечислять не интересно.

1
Начать дискуссию