Embedded Objects
Status: Offen
Bedarf aus den Samples
| Sample | Entity | Feld | Struktur |
|---|---|---|---|
| mietnomade | property | address | { street, zip, city, country } |
| mietnomade | contact | address | { street, zip, city, country } |
| mietnomade | lease | allocationKeys[] | [{ costCategory, allocationKey, customValue }] |
| beammycar | order | vehicle | { fin, licensePlate, notReadyToDrive, ... } (7 Felder) |
| beammycar | order | pickup | { address, contact, phone, dateTime, latestDateTime } |
| beammycar | order | delivery | { address, contact, phone, dateTime, ..., washed, fullFuel, ... } (9 Felder) |
| beammycar | invoice | issuer | { name, street, city, zip, vatId, iban, bic } |
| beammycar | invoice | customer | { name, street, city, zip, country, vatId } |
Muster
- Wiederverwendbare Strukturen:
addresskommt 5x vor (property, contact, order.pickup, order.delivery, invoice) - Entity-spezifische Gruppen:
vehicle,deliverysind unique fuer order - Verschachtelung: order.pickup.address waere 2 Ebenen tief
- Arrays: lease.allocationKeys ist ein Array von Embedded Objects
Weg A: JSONB-Spalte mit Zod-Schema
Jedes Embedded Object wird als eine JSONB-Spalte in der DB gespeichert. Das Zod-Schema validiert die Struktur.
API
r.entity("property", { fields: { name: { type: "text", required: true }, address: { type: "embedded", schema: { street: { type: "text", required: true }, zip: { type: "text", required: true }, city: { type: "text", required: true }, country: { type: "text", default: "DE" }, }, }, },});DB
-- Eine Spalteaddress JSONB DEFAULT '{}' NOT NULLZod (auto-generiert)
z.object({ street: z.string(), zip: z.string(), city: z.string(), country: z.string().default("DE"),})Vorteile
- Einfach zu implementieren (JSONB ist schon da fuer extendSchema)
- Verschachtelung (pickup.address) trivial — JSONB in JSONB
- Arrays (allocationKeys[]) trivial —
z.array(embeddedSchema) - Wiederverwendbare Schemas:
const addressSchema = { street, zip, city, country }als Constant - Migration: Eine Spalte hinzufuegen, nicht 4
- Kein JOIN noetig
Nachteile
- Kein DB-Level Index auf Einzelfelder (z.B. WHERE address->>‘city’ = ‘Berlin’ braucht GIN Index)
- Kein Foreign Key moeglich auf Embedded-Felder
- Search: Meilisearch muss Embedded-Felder flach bekommen (address.city → address_city)
- Field Access: Zugriff auf Einzelfelder (z.B. iban innerhalb von issuer) braucht Pfad-Syntax
Offene Fragen
- Kann der Table-Builder JSONB-Spalten mit NOT NULL + Default erzeugen? (Ja, extendSchema macht das schon)
- Wie mapped der Schema-Builder JSONB → Zod? (Neuer Code noetig)
- Wie funktioniert Search-Indexing fuer Embedded-Felder?
Weg B: Flache Spalten mit Prefix
Jedes Embedded-Feld wird zu mehreren Spalten mit Prefix. In der Entity-Definition gruppiert, in der DB flach.
API
r.entity("property", { fields: { name: { type: "text", required: true }, address: { type: "embedded", schema: { street: { type: "text", required: true }, zip: { type: "text", required: true }, city: { type: "text", required: true }, country: { type: "text", default: "DE" }, }, }, },});DB
-- Vier Spaltenaddress_street TEXT NOT NULL,address_zip TEXT NOT NULL,address_city TEXT NOT NULL,address_country TEXT DEFAULT 'DE' NOT NULLZod (auto-generiert)
// Input/Output hat verschachtelte Strukturz.object({ address: z.object({ street: z.string(), zip: z.string(), city: z.string(), country: z.string().default("DE"), }),})Mapping-Layer
// Entity-Daten (verschachtelt) → DB-Row (flach){ address: { street: "Hauptstr.", city: "Berlin" } }→ { address_street: "Hauptstr.", address_city: "Berlin" }
// DB-Row (flach) → Entity-Daten (verschachtelt){ address_street: "Hauptstr.", address_city: "Berlin" }→ { address: { street: "Hauptstr.", city: "Berlin" } }Vorteile
- Jedes Feld einzeln indexierbar (WHERE address_city = ‘Berlin’)
- Standard-SQL, keine JSONB-Abhaengigkeit
- Field Access pro Sub-Feld funktioniert mit bestehendem System (address_city als eigenes Feld)
- DB Dialect agnostisch — funktioniert auch mit MySQL, SQLite (kein JSONB noetig)
- Migration: Einzelne Spalten hinzufuegen/entfernen wie normale Felder
Nachteile
- Verschachtelung (2+ Ebenen) wird hässlich:
order_pickup_address_street - Arrays unmoeglich —
allocationKeys[]kann nicht als flache Spalten abgebildet werden - Mapping-Layer noetig (flatten/unflatten) in jedem DB-Zugriff
- Viele Spalten: order hat 20+ Felder wenn pickup + delivery + vehicle flach werden
- Wiederverwendung: addressSchema muss trotzdem definiert werden, spart aber keine Spalten
Offene Fragen
- Wie geht der Table-Builder mit Prefix um? (Neuer Code: iteriert ueber Schema, erzeugt prefixed Columns)
- Wie funktioniert Sorting/Filtering? (address_city als normales Feld, intuitiv)
- Was passiert bei 2+ Ebenen? (order.pickup.address → order_pickup_address_street — funktioniert, aber unschoen)
Vergleich
| Kriterium | Weg A (JSONB) | Weg B (Flach) |
|---|---|---|
| Implementierung | Einfacher (1 Spalte) | Aufwaendiger (Mapping-Layer) |
| Arrays (allocationKeys[]) | Trivial | Unmoeglich |
| Verschachtelung (2 Ebenen) | Trivial | Hässlich aber machbar |
| DB-Indexing | GIN Index, weniger performant | Standard Index, performant |
| Field Access (Einzelfelder) | Pfad-Syntax noetig | Funktioniert wie bisher |
| DB-Dialect-Kompatibilitaet | Nur PG/MySQL (JSONB) | Ueberall |
| Migration | 1 Spalte | n Spalten |
| Search-Indexing | Flattening noetig | Direkt |
Empfehlung
Weg A (JSONB) deckt alle Sample-Anforderungen ab, inkl. Arrays und Verschachtelung. Weg B scheitert an allocationKeys[] und wird bei beammycar-order (3 Embedded Groups) unhandlich.
Wenn DB-Dialect-Kompatibilitaet (SQLite, MySQL) wichtig ist: Hybrid — JSONB als Default, flache Spalten als Opt-In fuer indexierte Felder.