JDBC to JDBC
Use this recipe when you need a batch migration between relational databases and want to filter or reshape rows before writing them. The example reads orders from MySQL, keeps only paid orders, normalizes customer names, fixes the amount scale, fills a source-system field, and writes the result to PostgreSQL.
Prerequisites
Finish Run your first job.
Install the JDBC connector. Follow Deployment > Download The Connector Plugins, then keep the plugin below in
config/plugin_config:
--seatunnel-connectors--
connector-jdbc
--end--
cd "${SEATUNNEL_HOME}"
sh bin/install-plugin.sh
ls connectors | rg 'connector-jdbc'
The Sql transform is included in the SeaTunnel distribution and does not require another connector entry.
- Put both database drivers into
${SEATUNNEL_HOME}/lib, then confirm that SeaTunnel can see them:
ls "${SEATUNNEL_HOME}/lib" | rg 'mysql-connector|postgresql'
- Create the MySQL source table and insert three deterministic rows:
CREATE DATABASE IF NOT EXISTS source_db;
CREATE TABLE IF NOT EXISTS source_db.orders (
id BIGINT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
amount DECIMAL(16, 4) NOT NULL,
status VARCHAR(20) NOT NULL
);
TRUNCATE TABLE source_db.orders;
INSERT INTO source_db.orders (id, customer_name, amount, status) VALUES
(1001, 'alice chen', 120.5000, 'PAID'),
(1002, 'bob li', 80.0000, 'CREATED'),
(1003, 'carol wu', 42.0000, 'PAID');
- Create the PostgreSQL target database and table. The sink user needs
INSERTpermission on this table.
CREATE USER test WITH PASSWORD 'test';
CREATE DATABASE target_db OWNER test;
Reconnect to target_db as test, then run:
CREATE TABLE IF NOT EXISTS public.paid_orders (
id BIGINT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
source_system VARCHAR(20) NOT NULL
);
TRUNCATE TABLE public.paid_orders;
If the user or database already exists, reuse it instead of running the corresponding CREATE statement.
Complete configuration
The plugin_output and plugin_input values connect the three stages. The transformed column order also matches the four placeholders in the sink query; keep those two orders aligned when adapting the example.
env {
parallelism = 1
job.mode = "BATCH"
}
source {
Jdbc {
plugin_output = "mysql_orders"
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://mysql.example.com:3306/source_db?useSSL=false&allowPublicKeyRetrieval=true"
username = "root"
password = "password"
query = "SELECT id, customer_name, amount, status FROM orders"
}
}
transform {
Sql {
plugin_input = "mysql_orders"
plugin_output = "paid_orders"
query = """
SELECT
id,
UPPER(customer_name) AS customer_name,
CAST(amount AS DECIMAL(12, 2)) AS amount,
'MYSQL' AS source_system
FROM dual
WHERE status = 'PAID'
"""
}
}
sink {
Jdbc {
plugin_input = "paid_orders"
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://postgresql.example.com:5432/target_db"
username = "test"
password = "test"
query = "INSERT INTO public.paid_orders (id, customer_name, amount, source_system) VALUES (?, ?, ?, ?)"
}
}
dual is the virtual input table used by the default SeaTunnel SQL transform engine. It does not refer to a MySQL or PostgreSQL table.
Run the job
Save the configuration as config/jdbc-to-jdbc.conf, replace the hostnames and credentials with values for your environment, and run SeaTunnel in local mode:
cd "${SEATUNNEL_HOME}"
./bin/seatunnel.sh --config ./config/jdbc-to-jdbc.conf -m local
Verify the result
Query the PostgreSQL target after the job finishes:
SELECT id, customer_name, amount, source_system
FROM public.paid_orders
ORDER BY id;
Expected result:
| id | customer_name | amount | source_system |
|---|---|---|---|
| 1001 | ALICE CHEN | 120.50 | MYSQL |
| 1003 | CAROL WU | 42.00 | MYSQL |
This result verifies each transformation independently: order 1002 was filtered out because it was not paid, names were uppercased, amounts have scale 2, and the constant source_system field was added.
Common pitfalls
- The MySQL or PostgreSQL driver is missing from
${SEATUNNEL_HOME}/lib, or its version is incompatible with the database. - An example hostname is copied without being replaced by an address that the SeaTunnel process can resolve.
- The sample MySQL URL disables TLS and allows public-key retrieval for a simple development setup. Configure TLS according to your organization's security requirements before using the job in production.
- The selected field order changes, but the columns in the sink
INSERTstatement do not change with it. - The target table is not empty and a second run conflicts with its primary key. Truncate the table for a repeatable tutorial run, or choose an upsert strategy for production.
- A source
DECIMALvalue exceeds the precision or scale declared byDECIMAL(12, 2). Choose a target type that fits the real data before migrating it. - The source changes while a batch job is running. Use a CDC source instead when changes must be captured continuously.