Das Problem mit ORMs ist nicht, dass sie existieren.
Das Problem beginnt, wenn ein Team das ORM als Datenbankabstraktionsschicht behandelt, anstatt als Abfragegenerator, den man noch verstehen, messen und gelegentlich umgehen muss.
Diese Unterscheidung ist wichtig.
Worin ORMs tatsächlich gut sind
ORMs sind gut in:
- standardmäßigen CRUD-Pfaden
- Schema-Modellierung
- Migrationen
- Reduzierung von sich wiederholendem Mapping-Code
Sie sind normalerweise schlecht darin, das Kostenmodell relationaler Datenbanken zu verschleiern.
Wenn ein Team niemals das SQL betrachtet, wird das ORM zu einem Latenzgenerator mit schönem Autocomplete.
Der Fehlermodus, den jeder irgendwann sieht
Das klassische Beispiel ist das N+1-Abfrageproblem:
const users = await db.user.findMany({ take: 50 });
for (const user of users) {
console.log(user.invoices[0]?.status);
}
Auf der Anwendungsebene sieht das harmlos aus.
Auf der Datenbankebene bedeutet es oft:
- eine Abfrage, um Benutzer abzurufen
- fünfzig weitere Abfragen, um verwandte Datensätze abzurufen
Das ist kein ORM-Bug. Es ist das Ergebnis des Zugriffs auf Beziehungen, der die Abfragegrenzen verbirgt.
Eager Loading ist keine universelle Lösung
Die übliche Reaktion ist, alles im Voraus zu laden:
const users = await db.user.findMany({
include: {
invoices: true,
},
take: 50,
});
Das reduziert die Anzahl der Hin- und Rückreisen, kann aber ein anderes Problem schaffen:
- viel größere Ergebnismengen
- duplizierte Daten über Joins hinweg
- teure Rekonstruktion im Speicher
Die wirkliche Lektion ist also nicht "immer Eager Load". Es ist "verstehe den Abfrageplan, den du gerade angefordert hast."
Wann Roh-SQL oder ein Query-Builder überlegen ist
Sobald eine Abfrage:
- aggregationsintensiv
- join-intensiv
- latenzsensitiv
- reportähnlich
wird, möchte man sie in der Regel explizit gestalten.
Ein typisierter Query-Builder ist oft ein guter Mittelweg:
const rows = await db
.selectFrom("users as u")
.leftJoin("orders as o", "o.user_id", "u.id")
.select(({ fn }) => [
"u.id",
"u.email",
fn.coalesce(fn.sum("o.total"), 0).as("lifetime_value"),
])
.groupBy(["u.id", "u.email"])
.execute();
Der Wert hier ist nicht, dass SQL moralisch überlegen ist. Es ist, dass man gezwungen ist, über die Struktur der Abfrage nachzudenken.
Die praktische Regel
Verwenden Sie das ORM, wo es hilft.
Fallen Sie zurück, wenn die Abfrage erstklassige Aufmerksamkeit verdient.
Gute Teams tun beides:
- ORM für routinemäßigen Datenzugriff
- explizites SQL für kritische Pfade
- Abfragedokumentation und Planinspektion für alles, was benutzerorientiert und langsam ist
Das Ingenieurversagen besteht nicht darin, ein ORM zu verwenden. Es ist, die Verantwortung für das SQL, das es erzeugt, aufzugeben.
Weiterführende Literatur