Applies to: IdM 16.0 and newer (backported to 15). For IdM versions that do not ship this task, use the standalone script variant – see Orphaned References Check (Standalone Script).
Task name (code): core-check-orphaned-references-long-running-task
Bean / class: eu.bcvsolutions.idm.core.scheduler.task.impl.CheckOrphanedReferencesTaskExecutor
An orphaned reference is a value in one record that points to another record which no longer exists – for example a contract whose identity_id refers to an identity that was deleted directly in the database, or a provisioning archive that points to a removed system.
In CzechIdM this situation is possible by design. The product deliberately does not create business foreign keys in the database (relations are mapped with @ForeignKey(ConstraintMode.NO_CONSTRAINT)), so referential integrity is enforced by the application layer (processors, events), not by the database engine. Whenever data is changed outside the application – manual SQL fixes, failed/partial imports, restores from a partial backup, an interrupted delete – dangling references can be left behind.
The Check Orphaned References task finds these dangling references automatically and, when explicitly allowed, cleans them up. It:
Typical uses: periodic data-quality checks, diagnosing “record not found” errors at load time, and cleaning data before an upgrade or a migration that will introduce real foreign keys.
Risk note. Cleanup uses native SQL and bypasses the application layer (processors, events, and the Envers audit trail). It is a low-level maintenance tool. Always run a preview first, review the report, and take a database backup before performing real changes.
Administration → Scheduled tasks → Add (or Run task), then pick Check orphaned references. It can be run once on demand or scheduled (e.g. a monthly preview whose report is e-mailed to administrators).
| Parameter (code) | Label in the UI | Type | Default | Meaning |
|---|---|---|---|---|
checkDirect | Direct references | boolean | true | Check direct references: @ManyToOne / @OneToOne and JOINED-inheritance links. |
checkPolymorphic | Polymorphic references | boolean | true | Check polymorphic references: a *_id column paired with a sibling *_type column (e.g. owner_id + owner_type). |
included | Searched entities | textarea | empty = all | Whitelist. Comma- or newline-separated entity names (e.g. IdmIdentityContract) or table names. Empty means every entity. |
ignored | Ignored entities | textarea | IdmAudit | Blacklist, subtracted from the searched set. |
deleteOrphans | Delete orphans (not null) | boolean | false | Allow the delete action (used when the reference column is NOT NULL). |
setNullOrphans | Null orphans (nullable) | boolean | false | Allow the set null action (used when the reference column is nullable). |
previewOnly | Preview only | boolean | true | Detect and report only; perform no change. The task changes nothing until this is turned off. |
sampleSize | Sample id count | number | 5 | How many source record ids are listed per reference in the XLSX report. Item result messages always show at most 5. |
When orphaned records are found for a reference, the action is derived from the column nullability, and the JPA model and the database must agree:
| Model nullability | Database nullability | Action | Enabled by |
|---|---|---|---|
| nullable | nullable | set null – clear the reference column | setNullOrphans |
| NOT NULL | NOT NULL | delete – delete the whole orphaned record | deleteOrphans |
| any | differs from model | none – item ends as EXCEPTION (mismatch) | – |
For a polymorphic reference the set null action also clears the sibling *_type column (when it is nullable), so the row is left as a fully empty reference rather than a type without an id.
previewOnly = true). Nothing is changed. Optionally narrow the scope with included.previewOnly = false and enable deleteOrphans and/or setNullOrphans as needed.Example 1 – full preview (safest, recommended first run).
checkDirect = true checkPolymorphic = true included = (empty) ignored = IdmAudit deleteOrphans = false setNullOrphans = false previewOnly = true
Detects every orphaned reference in the whole model and writes the report; changes nothing. Items appear as EXECUTED (clean), NOT_EXECUTED (would be resolved) or BLOCKED (found, but no action enabled).
Example 2 – resolve nullable orphans on one entity.
included = IdmIdentityContract deleteOrphans = false setNullOrphans = true previewOnly = false
Only IdmIdentityContract references are checked. Nullable orphaned columns are set to NULL; NOT-NULL orphans are reported as BLOCKED (delete is not enabled) so nothing is deleted.
Example 3 – delete NOT-NULL orphans across the model.
deleteOrphans = true setNullOrphans = true previewOnly = false
Performs both actions everywhere: nullable orphans are nulled, NOT-NULL orphans (whole records) are deleted. Use only after a reviewed preview and a backup.
*_type value found in the data). Item state:orphaned-references-report.xlsx). Columns: Entity, Attribute, Referenced entity, Action, State, Orphan count, Sample IDs. The report is produced only when at least one orphan or one failure was found.
Discovery is implemented by DefaultOrphanedReferenceService (bean orphanedReferenceService). It scans EntityManager.getMetamodel() once and caches the result (the metamodel is fixed for the application runtime). For every entity with a physical @Table(name) it collects:
@ManyToOne / @OneToOne attributes declared on the entity and on its @MappedSuperclass ancestors (those columns live in the entity table). The join column is taken from @JoinColumn(name) or, if absent, the JPA default (<attribute>_id, snake-cased).subtable.id → parenttable.id is added.UUID column named *_id that has a sibling basic String column *_type with the same prefix. The concrete target table is resolved per distinct *_type value at processing time.
References are deduplicated by (table, column) (e.g. thin entities map the same table). The following are intentionally skipped (logged at debug/warn): attributes with @Formula (no physical column), inverse @OneToOne(mappedBy=…) sides, reversed @JoinColumn(referencedColumnName=…) mappings (e.g. the forest-index id link), targets without @Table(name), and any table/column that is not a plain SQL identifier.
Orphans are detected with a storage-agnostic anti-join (no literal ids, no casts on the join):
SELECT COUNT(*) FROM <table> s WHERE s.<column> IS NOT NULL [ AND s.<type_column> = :TYPE ] -- polymorphic only AND NOT EXISTS (SELECT 1 FROM <target> t WHERE t.id = s.<column>);
Cleanup actions:
DELETE FROM <table> s WHERE <same where>; -- both NOT NULL UPDATE <table> s SET <column> = NULL [, <type_column> = NULL] -- both nullable WHERE <same where>;
Column nullability is read once from information_schema.columns for current_schema(). Sample ids for the report are aggregated with string_agg; UUID ids (stored as bytea) are rendered via encode(id,'hex') and formatted back to UUID, other id types (e.g. a bigint forest index) via CAST(id AS varchar).
information_schema, current_schema(), string_agg, encode). From IdM 16 PostgreSQL is the only supported database.REQUIRES_NEW transaction, so that for a polymorphic reference with several types the failure of one type does not roll back the already-resolved sibling types.supportsDryRun() returns false on purpose – the built-in dry run would skip the per-item detection. The dedicated previewOnly parameter is used instead (and is on by default).continueOnException() is true – one failing reference does not stop the whole run; it is logged as an EXCEPTION item and counted in failed. Result code (CoreResultCode) | State | When |
|---|---|---|
| (no model) | EXECUTED | reference is clean (0 orphans) |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_RESOLVED | EXECUTED | action performed (preview off) |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_WOULD_RESOLVE | NOT_EXECUTED | preview – action would run |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_BLOCKED | BLOCKED | orphans found, action disabled |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_MISMATCH | EXCEPTION | model vs database nullability mismatch |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_CHECK_FAILED | EXCEPTION | the check/action threw an error |
LONG_RUNNING_TASK_ORPHANED_REFERENCES_SUMMARY | (task result, 206) | final summary + report download link |
*_id/*_type pairs) are discovered. References held purely in text/JSON or in configuration values are not covered.
Because discovery is driven by the metamodel, new entities and relations are covered automatically. No task change is needed when a new @ManyToOne or a new polymorphic *_id/*_type pair is added, as long as the entity has a physical @Table(name) and plain SQL identifiers.
See also: Orphaned References Check (Standalone Script) – the standalone Groovy script that provides the same detection as a downloadable SQL preview and runs on IdM versions without this task.