dsp-job-data-migration β Generic layer migration¶
Tutorial guide for the geographic layers module (batch.layers) of the dsp-job-data-migration job. Job overview: Overview.
What this module does¶
The job reads extra PostGIS tables in the source database and replicates their features in the geo-target database (display / WMS).
You specify role columns (PK, link to AOI, creation date, geometry). The job discovers the rest of the schema and creates the destination table.
| You specify in YAML | The job discovers |
|---|---|
| Source table name | Other columns and types (via introspection + additional-columns) |
| PK, AOI link, creation, geometry | Indexes on destination |
| (Optional) SRID, filters, label, extras | β |
Important concepts¶
| Term | Meaning | Example |
|---|---|---|
| Layer | A whole geographic table | conservation.rivers |
| Feature | One row (record) in the layer | A river segment |
| Area of interest (AOI) | Canonical entity in the DSP | dsp.area_of_interest |
Assumption: each feature belongs to one area of interest. The column that links it at the source is declared in YAML; on the destination it always becomes area_of_interest_id.
In the core wizard (adopter-config.yaml), this field is called parent_key and is translated to area-of-interest-id-column.
Where data lands¶
flowchart LR
src[("Source<br/>source")]
geo[("Geo-target<br/>WMS display")]
dsp[("DSP DB<br/>operational")]
src -->|"Fixed jobs<br/>(AU, AOI)"| dsp
src -->|"Fixed jobs"| geo
src -->|"Generic layers<br/>(this module)"| geo
| Destination | Writer | Notes |
|---|---|---|
DSP DB (target) |
Fixed jobs (admin units, AOI) | API business data |
Geo-target (geo-target) |
Fixed jobs and generic layers | Geometries for map |
Generic layers only write to geo-target. Destination comes from resolved layer-name (if omitted, source table name), hyphen β underscore:
Examples: source conservation.rivers without layer-name β dsp.rivers; layer-name: tipo-a β dsp.tipo_a. The same source-table can appear in more than one entry if destinations differ.
Canonical columns on destination: id (varchar(255)), area_of_interest_id (varchar(255)), created_at (timestamptz), updated_at (if present), label (if present), geom. Only one geometry is migrated β the one in geometry-column.
Execution order¶
Layers depend on area_of_interest_id pointing to records that already exist in dsp.area_of_interest.
1. Admin units (level-1 β level-2 β level-3)
2. Area of interest (area-of-interest)
3. Generic layers (layer-jobs) β this module
4. KPI calculation (kpi-job) β after the layers used by themes
In practice, fixed jobsβ JobRunner runs before the layer runner (@Order(1) and @Order(2)); kpiCalculationJob runs after (@Order(3)). Each theme KPI in the wizard points at the layer-name of a layer migrated here.
Prerequisites¶
- Source database with PostGIS and tables to migrate
- Geo-target database reachable (
spring.datasource.geo-target) - Each source table with simple PK (one column)
- Geometry column specified in YAML
- AOI link column filled on features
-
creation-date-columnfilled (watermark) - Area of interest job already run (valid FK values)
- Spring Batch schema created on
dsp-db(data_migration)
Minimum configuration¶
1. Geo-target datasource¶
Besides batch, source, and target, configure:
spring:
datasource:
geo-target:
url: jdbc:postgresql://localhost:5432/dsp-geoserver-db
username: dsp_geo
password: dsp_geo
2. Declare layers¶
batch:
layers:
- source-table: conservation.rivers
primary-key: feature_id
area-of-interest-id-column: conservation_unit_id
creation-date-column: created_at
geometry-column: geom
layer-name: rivers
srid: 4674
| Property | Required | Description |
|---|---|---|
source-table |
yes | Source table as schema.table (exactly one .) |
primary-key |
yes | Simple PK at source |
area-of-interest-id-column |
yes | Source column identifying the featureβs AOI |
creation-date-column |
yes | Creation column β watermark basis |
geometry-column |
yes | Which source column becomes geom on destination |
3. Enable execution¶
4. Parallelization (optional)¶
Job name follows layerMigrationJob_ + layer key (dsp_<physical_name>):
parallelization:
jobs:
layerMigrationJob_dsp_rivers:
enabled: true
thread-pool-size: 2
chunk-size: 100
page-size: 1000
queue-capacity: 100
If there is no entry for a layer, the job uses defaults.
Optional properties¶
| Property | Default | When to use |
|---|---|---|
layer-name |
table name | WMS identity (dsp:<name>) and physical destination base (dsp.<name> with hyphen β _) |
srid |
discovered at source | Destination SRID. On read, ST_Transform reprojects source geometry to this value (same as AU/AOI) |
updated-at-column |
β | Incremental by update as well |
label-column |
β | Display text (label on destination) |
additional-columns |
[] |
Extra columns copied with same name |
where-clause |
1=1 |
Feature subset (detection, orphans, and writes) |
source-timezone |
batch.source-timezone |
Temporal columns without offset |
enabled |
true |
Only in manually edited application.yaml. In wizard/adopter-config.yaml, remove the layer from etl.layers[] instead of using enabled |
If the destination table already exists, the job requires created_at timestamptz on it (CREATE TABLE IF NOT EXISTS does not alter an existing table).
What happens on each run¶
Each layer gets one independent Spring Batch job.
flowchart TD
A[Setup] --> B[Watermark change detection]
B --> C{Temporal delta?}
C -->|No| D[End β SKIP<br/>orphans may already have been deleted]
C -->|Yes| E[Partitioned read]
E --> F[UPSERT on geo-target]
Step 1 β Setup¶
- Validates table exists at source
- Discovers columns, PK, geometry, SRID, and indexes
- Runs
CREATE SCHEMA IF NOT EXISTS dspon geo-target - Creates table
dsp.<physical_name>(if not exists) - Creates GIST index on
geomand index onarea_of_interest_id
Step 2 β Change detection¶
Same engine as admin units and AOI:
- Delta β records with creation (or
updated_at) after watermark stored inBATCH_JOB_EXECUTION_SYNC_STATE - Orphans β PK only on geo-target β DELETE (scan every 24 h)
- No delta β SKIP (even if orphans were deleted)
Change detection scope
Orphan removal happens only on geo-target. DSP DB is not affected by this module.
Step 3 β Load (UPSERT)¶
- Reads features from source in pages (partitioning when PK is numeric or numeric VARCHAR)
- Writes to geo-target with
INSERT ... ON CONFLICT DO UPDATE - Renames link column: e.g.
conservation_unit_idβarea_of_interest_id - Geometries with Z/M are flattened to 2D (
ST_Force2D)
Example with multiple layers¶
batch:
layers:
- source-table: conservation.rivers
primary-key: feature_id
area-of-interest-id-column: conservation_unit_id
creation-date-column: created_at
geometry-column: geom
layer-name: rivers
srid: 4674
- source-table: conservation.lakes
primary-key: feature_id
area-of-interest-id-column: conservation_unit_id
creation-date-column: created_at
geometry-column: geom
layer-name: lakes
srid: 4674
execution-jobs:
admin-unit-level-1-geoserver-job: true
admin-unit-level-2-geoserver-job: true
admin-unit-level-3-geoserver-job: true
area-of-interest-geoserver-job: true
layer-jobs: true
parallelization:
jobs:
layerMigrationJob_dsp_rivers:
enabled: true
thread-pool-size: 1
chunk-size: 100
page-size: 1000
layerMigrationJob_dsp_lakes:
enabled: true
thread-pool-size: 1
chunk-size: 100
page-size: 1000
How to run¶
At the root of the dsp-job-data-migration repository:
# 1. Spring Batch metadata on destination database (once)
psql -h localhost -U postgres -d dsp_db \
-f src/main/resources/db/batch_metadata/01_spring_batch_schema.sql
# 2. Start the job
./mvnw spring-boot:run
To run only layers (fixed jobs off):
./mvnw spring-boot:run -Dspring-boot.run.arguments="\
--execution-jobs.admin-unit-level-1-geoserver-job=false \
--execution-jobs.admin-unit-level-2-geoserver-job=false \
--execution-jobs.admin-unit-level-3-geoserver-job=false \
--execution-jobs.area-of-interest-geoserver-job=false \
--execution-jobs.layer-jobs=true"
How to validate¶
On geo-target, check table was created and populated:
SELECT COUNT(*) FROM dsp.rivers;
SELECT area_of_interest_id, COUNT(*)
FROM dsp.rivers
GROUP BY area_of_interest_id;
SELECT COUNT(*) FROM dsp.rivers WHERE geom IS NOT NULL;
Layer watermark (on dsp-db):
SELECT sync_key, watermark_last_event_at, last_success_at, last_orphan_check_at
FROM data_migration.BATCH_JOB_EXECUTION_SYNC_STATE
WHERE source_table LIKE '%rivers%';
Useful logs (package br.car.dsp_batch):
Introspection completed for ...β structure discoveredTarget table ready: dsp.riversβ DDL appliedUpserted N features into geo-target dsp.riversβ load completeNo changes detectedβ re-run with no temporal delta (SKIP)
Current limitations¶
| Situation | Behavior |
|---|---|
| Composite PK | Error β not supported |
| UUID / non-numeric text PK | Single partition (no range parallelism) |
| Table gains new columns later | CREATE TABLE IF NOT EXISTS does not alter existing table |
| Multiple geometries at source | Only geometry-column is migrated; others ignored |
| Source SRID β YAML | ST_Transform on read to YAML srid (same as AU/AOI) |
| GeoServer / cache | This module does not trigger cache refresh |
Common errors¶
| Message / symptom | Likely cause | What to do |
|---|---|---|
area-of-interest-id-column is required |
Incomplete YAML | Specify AOI link column |
Missing creation-date-column |
Incomplete YAML | Specify creation column |
Source table not found |
Wrong schema/table | Check source-table and permissions |
has no PRIMARY KEY |
Table without PK | Declare primary-key in YAML |
SRID not found |
Empty geometry or no SRID | Set srid explicitly |
| Job starts but does not migrate layers | layer-jobs: false or layer with enabled: false in manual YAML |
Check flags; in wizard, remove layer from etl.layers[] |
| Invalid FK on map | AOI not migrated first | Run area-of-interest job first |
ON CONFLICT fails |
Missing PK on destination | Drop destination table and run Setup again |
Existing destination without created_at timestamptz |
Old table | Fix column or recreate table |
Difference from fixed jobs¶
| Fixed jobs (AU, AOI) | Generic layers | |
|---|---|---|
| Configuration | AU: column by column; AOI: canonical roles | Table + PK + AOI + creation + geometry |
| Destination | DSP DB and geo-target | Geo-target only |
| DDL | AU: core SQL; AOI: automatic | Created automatically |
| Mapping | AU: column-mapping; AOI: canonical |
Mirrors source (except roles β id / area_of_interest_id / geom) |
| Change detection | Temporal watermark | Temporal watermark |
In the dsp-core wizard¶
In stage 2/6 (etl.layers[] in adopter-config.yaml), each layer is configured once and feeds migration, map, and downloads:
| Wizard / YAML field | Use |
|---|---|
source_table, primary_key, parent_key (FK β AOI), temporal columns, geometry_column, where_clause, srid |
Job (application.yaml) |
layer_name |
Technical WMS id (dsp:<name>) and destination table dsp.<name> |
display_name |
Human label in layer selector and Downloads screen |
group_key |
Map group (existing or new) |
active_default, color, fill_color |
Layer on by default and WMS style |
The same source_table can repeat with a different layer_name; the wizard reuses column mapping from the previous layer with the same source.
To turn off a layer in the wizard flow, remove it from etl.layers[] and reapply ./config.sh β enabled is not accepted in adopter-config.yaml.
Full wizard detail: dsp-core.
Where to go deeper¶
| Document | Content |
|---|---|
| Overview | Job logic, watermark, execution in core |
| Overview | Order and watermark |
| Post-migration validation | Post-migration checklist |