Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

🗄️ The JPA Enum Default Quietly Corrupts Your Data

You add an enum to an entity, slap @Enumerated on it, and move on. Five seconds. It is the kind of decision nobody writes a design doc for. Then six months later a row comes back as SHIPPED when it was PAID, no exception was thrown, no…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

You add an enum to an entity, slap @Enumerated on it, and move on. Five seconds. It is the kind of decision nobody writes a design doc for.



Then six months later a row comes back as SHIPPED when it was PAID, no exception was thrown, no query failed, and you spend an afternoon learning that the default you never thought about has been silently rewriting history.



Here is the order lifecycle we will use the whole way through:




public enum OrderStatus {
PENDING,
PAID,
SHIPPED,
DELIVERED
}






Five ways to store it. They are not equivalent, and the gap between them only shows up under change.






@Enumerated(ORDINAL): store the position



This is the default. Leave the annotation bare and JPA stores the enum's ordinal, its index in the declaration order.




@Enumerated(EnumType.ORDINAL)
private OrderStatus status;






PENDING is 0, PAID is 1, SHIPPED is 2, DELIVERED is 3. The column is a tidy little smallint. Everything works.



Until someone needs a new status and adds it where it reads well:




public enum OrderStatus {
PENDING,
PAID,
CANCELLED, // inserted here
SHIPPED,
DELIVERED
}






CANCELLED is now 2. SHIPPED is 3. DELIVERED is 4. Every row written before this change still holds the old integer, so every order that was SHIPPED (2) now reads back as CANCELLED. The database is correct. Your data is wrong. And nothing told you.



If you are stuck with ORDINAL on a legacy schema, pin it with a test that fails the build the moment someone reorders:




@Test
void ordinalsAreFrozen() {
assertEquals(0, OrderStatus.PENDING.ordinal());
assertEquals(1, OrderStatus.PAID.ordinal());
assertEquals(2, OrderStatus.SHIPPED.ordinal());
assertEquals(3, OrderStatus.DELIVERED.ordinal());
}






New constants may only be appended. The test turns an invisible runtime corruption into a loud compile-time-ish failure. It is a guardrail, not a fix.






@Enumerated(STRING): store the name



Store the constant name instead of its position.




@Enumerated(EnumType.STRING)
private OrderStatus status;






Now the column holds 'PAID'. Reordering the enum is free, because the name does not move when the position does. The column is readable in a raw SELECT, exports document themselves, and a human debugging production can actually tell what a row means.



The cost moves to renames. Rename the constant:




PAID -> SETTLED






and every row still says 'PAID', which no longer matches any constant. Reads blow up or silently drop, depending on your mapping. A rename now requires a data migration:




UPDATE orders SET status = 'SETTLED' WHERE status = 'PAID';






For most schemas this is the right default. Renames are rarer than reorders, and when they happen they are at least visible.






A real Postgres enum type



The two options above are enforced entirely in the JPA layer. The database sees a varchar and will happily accept 'BANANA'. If you want the database itself to guarantee validity, give it a real type:




CREATE TYPE order_status AS ENUM ('PENDING', 'PAID', 'SHIPPED', 'DELIVERED');

CREATE TABLE orders (
id bigserial PRIMARY KEY,
status order_status NOT NULL
);






Now an invalid status is rejected at insert time, by the database, no matter which application or migration script tries to write it. Pair it with @Enumerated(STRING) on the Java side so the names line up.



One JDBC detail bites everyone the first time: the Postgres driver sends strings as varchar, and Postgres will not implicitly cast varchar to the enum type. Add stringtype=unspecified to the connection URL and the cast resolves:




jdbc:postgresql://localhost:5432/app?stringtype=unspecified






The limit: Postgres only lets you append values to an enum type. ALTER TYPE ... ADD VALUE works, but reordering or dropping a value means recreating the type and rewriting every column that uses it. You traded application-side flexibility for database-side guarantees.






AttributeConverter: decouple the name from the stored value



Both STRING and native enums tie the stored value to the Java identifier. Rename the constant, migrate the data. If you want renames to be free, stop storing the name at all. Store a stable code instead.




public enum OrderStatus {
PENDING("PND"),
PAID("PAID"),
SHIPPED("SHP"),
DELIVERED("DLV");

private final String code;

OrderStatus(String code) { this.code = code; }

public String getCode() { return code; }

public static OrderStatus fromCode(String code) {
for (OrderStatus s : values()) {
if (s.code.equals(code)) return s;
}
throw new IllegalArgumentException("Unknown code: " + code);
}
}









@Converter(autoApply = true)
public class OrderStatusConverter
implements AttributeConverter<OrderStatus, String> {

@Override
public String convertToDatabaseColumn(OrderStatus status) {
return status == null ? null : status.getCode();
}

@Override
public OrderStatus convertToEntityAttribute(String code) {
return code == null ? null : OrderStatus.fromCode(code);
}
}






The column stores 'PND'. The Java identifier is now free to change:




PENDING -> AWAITING_PAYMENT






The code stays "PND", the database does not move, the rename is a pure refactor your IDE does in one shortcut. The price is one extra class per enum and a layer of indirection: the code in the column no longer reads like the constant, so a raw SELECT shows 'PND' instead of 'PENDING'. You bought rename-freedom with a little readability.






The lookup table



The last option stops treating status as an enum at all. It becomes a foreign key to a reference table.




CREATE TABLE order_status (
id int PRIMARY KEY,
code varchar(32) UNIQUE NOT NULL,
label varchar(64) NOT NULL,
sort_order int NOT NULL,
is_active boolean NOT NULL DEFAULT true
);

CREATE TABLE orders (
id bigserial PRIMARY KEY,
status_id int NOT NULL REFERENCES order_status(id)
);






This is the one people reach for too early. It looks like the "grown-up" option, so it gets used as a general way to dodge renames and schema changes. That is the wrong reason. You pay a join on every read, you lose compile-time exhaustiveness (the compiler can no longer warn you about an unhandled switch branch), and you take on the overhead of keeping a relationship in sync.



A lookup table earns all of that only when the value carries editable business data. A display label the product team changes without a deploy. A sort order. A colour for the UI. A feature flag or an SLA attached to the status. Data that lives and changes at runtime.






The honest trade-off



There is no free option here, and the lookup table is where the cost is steepest. Every status read becomes a join. Every switch over status loses the safety net that makes enums worth using in Java. And you now own a tiny CRUD surface for reference data that, in most systems, never actually changes.



Weigh that against what STRING plus a Postgres enum type costs: a one-time migration on the rare day you rename a constant. For the overwhelming majority of status columns, that is the cheaper bill.






My take



Default to @Enumerated(STRING) backed by a native Postgres enum type. You get readable columns, reorder-safety, and the database rejecting garbage at the door.



Upgrade to an AttributeConverter the moment renames need to be free, for example a domain vocabulary that is still settling and gets renamed often.



Reach for a lookup table only when the value is genuinely a record: it has a label, a flag, an SLA, something a human edits at runtime.



That is the whole decision, compressed: if the value is identity only, keep it an enum. If the value is a record, it was never really an enum.



Get that right once and your statuses stay boring, which is the highest compliment you can pay a column.






What do you store status enums as in production, and has a reorder or a rename ever burned you?

1. Sofort-Triage & Abwehrmaßnahmen

SOC Incident Playbook: Remote Code Execution (RCE) Defense
Syntax validiert (0 Fehler)
title: Detect Exploitation - 🗄️ The JPA Enum Default Quietly Corrupts Your Data
id: 98b93dc7-c906-4ab3-9138-71ddca1e0e44
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-25
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
Syntax validiert (0 Fehler)
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-25"
        description = "YARA Signature for "
    strings:
        $str = "🗄️ The JPA Enum Default Quietl" ascii wide
    condition:
        any of them
}
Syntax validiert (0 Fehler)
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
("The JPA Enum Default Quietly Corrupts Yo")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
Syntax validiert (0 Fehler)
message: "*The JPA Enum Default Quietly Corrupts Yo*"
Syntax validiert (0 Fehler)
CommonSecurityLog
| where Message has "The JPA Enum Default Quietly Corrupts Yo"
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc

2. Cyber Threat Intelligence & Forensik

🎯
MITRE ATT&CK Matrix Navigator 14 Taktiken
Reconnaissance
-
Resource Development
-
Initial Access
Execution
Persistence
-
Privilege Escalation
Defense Evasion
Credential Access
-
Discovery
-
Lateral Movement
-
Collection
-
Command and Control
Exfiltration
-
Impact
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich 🗄️ The JPA Enum Default Quietly Corrupts.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten 🗄️ The JPA Enum Default Quietly Corrupts Your Data

Thematisch verwandte Begriffe: Enum, Default, Quietly, Corrupts · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-97818 | phpIPAM through 1.8.3 has incorrect authorization for id=="admins" and i…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag