Skip to content

Embedded Objects

Status: Offen

Bedarf aus den Samples

SampleEntityFeldStruktur
mietnomadepropertyaddress{ street, zip, city, country }
mietnomadecontactaddress{ street, zip, city, country }
mietnomadeleaseallocationKeys[][{ costCategory, allocationKey, customValue }]
beammycarordervehicle{ fin, licensePlate, notReadyToDrive, ... } (7 Felder)
beammycarorderpickup{ address, contact, phone, dateTime, latestDateTime }
beammycarorderdelivery{ address, contact, phone, dateTime, ..., washed, fullFuel, ... } (9 Felder)
beammycarinvoiceissuer{ name, street, city, zip, vatId, iban, bic }
beammycarinvoicecustomer{ name, street, city, zip, country, vatId }

Muster

  • Wiederverwendbare Strukturen: address kommt 5x vor (property, contact, order.pickup, order.delivery, invoice)
  • Entity-spezifische Gruppen: vehicle, delivery sind 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 Spalte
address JSONB DEFAULT '{}' NOT NULL

Zod (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 Spalten
address_street TEXT NOT NULL,
address_zip TEXT NOT NULL,
address_city TEXT NOT NULL,
address_country TEXT DEFAULT 'DE' NOT NULL

Zod (auto-generiert)

// Input/Output hat verschachtelte Struktur
z.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 unmoeglichallocationKeys[] 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

KriteriumWeg A (JSONB)Weg B (Flach)
ImplementierungEinfacher (1 Spalte)Aufwaendiger (Mapping-Layer)
Arrays (allocationKeys[])TrivialUnmoeglich
Verschachtelung (2 Ebenen)TrivialHässlich aber machbar
DB-IndexingGIN Index, weniger performantStandard Index, performant
Field Access (Einzelfelder)Pfad-Syntax noetigFunktioniert wie bisher
DB-Dialect-KompatibilitaetNur PG/MySQL (JSONB)Ueberall
Migration1 Spalten Spalten
Search-IndexingFlattening noetigDirekt

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.