Startseite / Artikel

Von Prisma zu Drizzle migrieren: Eine Rückblick auf sechs Monate

Sechs Monate nach dem Umstieg einer PostgreSQL-TypeScript-Stack-Struktur von Prisma auf Drizzle ORM: echte Benchmarks, was sofort besser wurde, und die Nachteile, die Probleme verursachten.

Verlag: REACTAPP.TOP10 Min. Lesezeit2403 Wörter

Quellen: I replaced Prisma with Drizzle. Six months later, here’s what I gained and what I missed.

Vor etwa acht Monaten kristallisierte sich ein ständig vorhandener Zweifel bezüglich Prisma schließlich zu etwas heraus, das es wert war, darauf zu handeln. Es lag nicht daran, dass etwas völlig kaputt war – der Widerstand hatte sich einfach im Laufe der Zeit allmählich aufgebaut, bis er nicht mehr ignoriert werden konnte. Die Infrastruktur bestand aus mehreren kleinen Anwendungen, die alle auf PostgreSQL liefen, in TypeScript geschrieben waren und hinter einigen API-Diensten standen. Nichts Ungewöhnliches oder Neuartiges. Prisma erledigte seine Aufgabe wie vorgesehen. Dennoch tauchten immer wieder einige störende Probleme auf.

Die erzeugte Datei @prisma/client war enorm groß. Beim Ausführen in serverless Funktionen führten die Kaltstarts zu erheblichen Verzögerungen, da zunächst der Client geladen werden musste. Der Schritt prisma generate im CI-Pipeline begann sich wie eine Gebühr anzuführen, die bei jedem Merge entrichtet werden musste. Jedes Mal, wenn eine Rohabfrage für etwas Komplexeres als das vom ORM Unterstützte benötigt wurde, kam der Rückgabetyp als unknown[] zurück – ohne jegliche Typsicherheit und mit der Notwendigkeit, überall manuelle Umwandlungen vorzunehmen.

Schließlich wurde beschlossen, auf Drizzle ORM umzusteigen; die Migration war etwa vor sechs Monaten abgeschlossen. Im Folgenden wird ehrlich beschrieben, wie das ablief.

Warum ich ursprünglich Drizzle in Betracht gezogen habe

Das Problem mit der Größe des Pakets war der erste wirkliche Anreiz, Drizzle ernst zu nehmen. Zum Zeitpunkt der Migration fügte allein der Prisma-Client fast 14 MB zum Lambda-Deployment-Paket hinzu, wobei das Binärdatei des Rust-Abfragemotors bereits für mehrere Megabyte sorgte. Endpunkte, die nicht häufig aufgerufen wurden, zeigten eine Ladezeit von 2 bis 3 Sekunden – ein Wert, der auf einem Latenz-Dashboard sofort ins Auge fällt.

Im Vergleich dazu beträgt die Größe von Drizzle nach Minifizierung und Komprimierung etwa 7,4 KB, ohne jegliche Laufzeitabhängigkeiten. Es gibt kein kompiliertes Binärdatei, keinen separaten Engine-Prozess und keine Serialisierungsstufe zwischen dem TypeScript-Code und der Datenbankverbindung.

Die zweite Quelle für Reibungsverluste war der Schritt der Codegenerierung. Jede Änderung am Schema bedeutete, dass man prisma generate ausführen musste, bevor TypeScript die neue Struktur erkennen konnte. In der Praxis führte das zu einem von zwei schlechten Ergebnissen: Entweder man führte den Befehl manuell aus und vergaß gelegentlich, die neu generierten Dateien zu speichern, oder man integrierte ihn in jeden CI-Ausführungsvorgang und musste pro Build zusätzlich 15 bis 20 Sekunden in Kauf nehmen. Keine der Optionen war zufriedenstellend.

Mit Drizzle besteht das Schema selbst aus TypeScript. Wenn man die Schema-Datei editiert, werden die Typen sofort aktualisiert – es gibt keinen Zwischenschritt der Generierung, und der Compiler erkennt die Änderung sofort.

Der dritte Anreiz betraf die Handhabung von rohem SQL. Prisma behandelt es ausschließlich als Notlösung: Die Aufrufung von $queryRaw gibt unknown[] zurück, was entweder einer manuellen Umwandlung oder einem Zod-Schema bedarf, um es sicher verwenden zu können. Der sql-Template-Tag von Drizzle hingegen ermöglicht es, die Struktur direkt anzugeben – etwa mit sql<{ id: number; total: number }> – und man erhält genau diesen Typ zurück, ohne Umwandlung und ohne zusätzliche Validierungsstufe.

Wie die Migration aussah

Zum Verständnis betrachten wir drei Tabellen: users, products und orders. Unter Prisma waren sie in einer einzigen schema.prisma-Datei definiert:

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  orders    Order[]
  createdAt DateTime @default(now())
}

model Product {
  id          String      @id @default(uuid())
  name        String
  priceInCents Int
  orders      OrderItem[]
}

model Order {
  id        String      @id @default(uuid())
  userId    String
  user      User        @relation(fields: [userId], references: [id])
  items     OrderItem[]
  createdAt DateTime    @default(now())
}

model OrderItem {
  id        String  @id @default(uuid())
  orderId   String
  order     Order   @relation(fields: [orderId], references: [id])
  productId String
  product   Product @relation(fields: [productId], references: [id])
  quantity  Int
}

Unter Drizzle wird dasselbe Schema zu einer TypeScript-Datei:

import { pgTable, uuid, text, integer, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';\

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const products = pgTable('products', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: text('name').notNull(),
  priceInCents: integer('price_in_cents').notNull(),
});

export const orders = pgTable('orders', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const orderItems = pgTable('order_items', {
  id: uuid('id').defaultRandom().primaryKey(),
  orderId: uuid('order_id').notNull().references(() => orders.id),
  productId: uuid('product_id').notNull().references(() => products.id),
  quantity: integer('quantity').notNull(),
});

export const usersRelations = relations(users, ({ many }) => ({
  orders: many(orders),
}));

export const ordersRelations = relations(orders, ({ one, many }) => ({
  user: one(users, { fields: [orders.userId], references: [users.id] }),
  items: many(orderItems),
}));

export const orderItemsRelations = relations(orderItems, ({ one }) => ({
  order: one(orders, { fields: [orderItems.orderId], references: [orders.id] }),
  product: one(products, { fields: [orderItems.productId], references: [products.id] }),
}));

Ja, es läuft etwas länger. Aber es handelt sich dabei von Anfang bis Ende um TypeScript. Ihr Editor versteht jedes Feld im Schema, die Autocomplete-Funktion arbeitet direkt damit zusammen, und Sie können Teile des Schemas direkt in die Logik Ihrer Anwendung importieren.

Eine Datei namens drizzle.config.ts bringt alles zusammen:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  dialect: 'postgresql',
  schema: './src/schema.ts',
  out: './drizzle/migrations',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
  strict: true,
});

Achtung: Setzen Sie immer strict: true. Wenn Sie das überspringen, könnte Drizzle eine Spaltenumbenennung als „alte Spalte löschen und neue erstellen“ interpretieren – eine Änderung, die Daten stillschweigend zerstört, bis Sie in den generierten SQL-Code schauen, um herauszufinden, was passiert ist. Durch Aktivierung von strict: true hält Drizzle bei jeder zweideutigen Änderung an und bittet um Bestätigung.

Sobald das Schema vorhanden ist, erfolgt die Erstellung einer Migration mit einem einzigen Befehl:

bunx drizzle-kit generate

Dadurch wird eine SQL-Datei in ./drizzle/migrations erzeugt. Sie prüfen diese, stellen sicher, dass sie das tut, was Sie erwarten, und wenden sie anschließend an:

bunx drizzle-kit migrate

Diese SQL-Datei wird direkt neben dem Schema in den Versionskontrollsystem eingetragen. Die gesamte Migrationsgeschichte besteht aus einfachem, lesbarer SQL statt eines Protokollformats, das nur mit Prismas eigenen Werkzeugen verständlich ist.

Was sich sofort verbessert hat

Kleinere Pakete, schnellere Kaltstartzeiten. Das war sofort offensichtlich und leicht messbar. Die Größe unserer Lambda-Pakete verringerte sich um mehr als 90 Prozent. Funktionen, die zuvor länger als zwei Sekunden zum Kaltstart benötigten, laufen nun in unter 400 Millisekunden. Hintergrundaufgaben profitierten nicht wirklich davon, aber unsere API-Funktionen wurden deutlich schneller.

Rohes SQL eingegeben. Dies erwies sich bereits am zweiten Tag der Migration als nützlich. Wir hatten eine Berichtsabfrage, die vier Tabellen mit Aggregationen verband, was der Prisma-Abfrageschreiber einfach nicht auf saubere Weise darstellen konnte. Der Prisma-Fallback für diese Art von Abfrage sah so aus:

// Prisma - returns unknown[], no type safety
const revenue = await prisma.$queryRaw`
  SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) as total_revenue
  FROM products p
  JOIN order_items oi ON p.id = oi.product_id
  GROUP BY p.id, p.name
  ORDER BY total_revenue DESC
`;

Mit Drizzle kommt die entsprechende Abfrage mit einem tatsächlichen Typ zurück:

import { sql } from 'drizzle-orm';

interface ProductRevenue {
  id: string;
  name: string;
  totalRevenue: number;
}

const revenue = await db.execute(sql<ProductRevenue>`
  SELECT p.id, p.name, SUM(oi.quantity * p.price_in_cents) AS total_revenue
  FROM products p
  JOIN order_items oi ON p.id = oi.product_id
  GROUP BY p.id, p.name
  ORDER BY total_revenue DESC
`);

// revenue.rows typed as ProductRevenue[] at compile-time; validate at runtime if needed

Man schreibt die Schnittstelle weiterhin manuell statt sie automatisch ableiten zu lassen, doch das ist ein geringer Aufwand im Vergleich dazu, unknown-Umwandlungen im gesamten Codebase zu verwenden.

Drizzle Studio. Die Ausführung von bunx drizzle-kit studio startet einen lokalen Datenbankbrowser. Er ist im Vergleich zu Prisma Studio weniger ausgereift, doch er ist schnell, funktional und erfordert weder ein Prisma-Konto noch eine Verbindung in die Cloud. Für den täglichen lokalen Einsatz ist diese Unabhängigkeit sehr wertvoll.

Was Anpassungen erforderte

Beziehungen benötigen explizitere Konfigurationen. In Prisma war es einfach, eine Bestellung zusammen mit ihrem Benutzer und den Bestellpositionen abzurufen, indem man einfach ein include verwendete:

// Prisma - nested include
const order = await prisma.order.findUnique({
  where: { id: orderId },
  include: {
    user: true,
    items: {
      include: { product: true }
    }
  }
});

Drizzles API für relationale Abfragen führt zu einem ähnlichen Ergebnis:

import { db } from './db';
import { orders, users, orderItems, products } from './schema';

const order = await db.query.orders.findFirst({
  where: (orders, { eq }) => eq(orders.id, orderId),
  with: {
    user: true,
    items: {
      with: { product: true }
    }
  }
});

Die endgültige Struktur der beiden Abfragen ist ähnlich. Das Problem ist, dass die with-Syntax nur dann funktioniert, wenn man die relations()-Aufrufe im Schema definiert hat – genau die, die bereits zuvor beschrieben wurden. Als wir zum ersten Mal darauf umstiegen, hatten wir diese noch nicht eingerichtet und verbrachten einen Nachmittag damit, herauszufinden, warum with nichts zurückgab. Sobald die Beziehungen ordnungsgemäß deklariert sind, funktioniert alles wie erwartet – doch dieser anfängliche Einrichtungsschritt wird von Prisma bereits implizit aus dem Schema selbst übernommen, ohne dass etwas zusätzlich konfiguriert werden muss.

Das Einpflegen von Daten ist nun eine manuelle Aufgabe. Prisma bietet eine integrierte Einpflegemethode mit: Legen Sie einfach eine seed.ts-Datei an den entsprechenden Ort, führen Sie prisma db seed aus – und es ist erledigt. Drizzle verfügt über keinen ähnlichen integrierten Befehl. Stattdessen muss man ein gewöhnliches TypeScript-Skript schreiben und es selbst ausführen:

import { db } from './db';
import { users, products } from './schema';

async function seed() {
  await db.insert(users).values([
    { email: 'ana.silva@acme.com', name: 'Ana Silva' },
    { email: 'carlos.reyes@acme.com', name: 'Carlos Reyes' },
  ]);

 await db.insert(products).values([
    { name: 'Starter Plan', priceInCents: 2900 },
    { name: 'Pro Plan', priceInCents: 7900 },
    { name: 'Enterprise Plan', priceInCents: 19900 },
  ]);

  console.log('Seed complete');
  process.exit(0);
}
seed().catch(console.error);

Und Sie konfigurieren es in package.json:

{
  "scripts": {
    "db:seed": "bun run src/seed.ts"
  }
}

Nichts davon ist schwierig, und man könnte argumentieren, dass es transparenter ist als Prismas implizite Konvention. Allerdings müssen alle vorhandenen Tools oder CI-Schritte, die direkt prisma db seed aufrufen, umgeschrieben werden, um stattdessen Ihr eigenes Skript zu verwenden.

Vergleich der beiden Tools direkt. Drizzle Studio erledigt die Aufgabe. Prisma Studio sieht einfach besser aus und bietet mehr: eingebaute Filtersteuerungen, Paginierung sowie einen anspruchsvolleren Tabelleneditor. Im Vergleich wirkt Drizzle Studio eher spartanisch. Nach sechs Monaten Nutzung von Drizzle sind wir weniger auf beide Tools angewiesen, als wir erwartet hatten, da die eigentliche Datenanalyse meist über einen speziellen Datenbankklienten erfolgt – unabhängig davon, welchen ORM man verwendet.

Der einzige Fall, in dem Prisma immer noch gewinnt

Nestierte Schreibvorgänge. Wenn Sie eine Bestellung zusammen mit ihren Positionen in einem Schritt einfügen müssen, erledigt Prisma das sauber:

// Prisma - nested create in one call
const order = await prisma.order.create({
  data: {
    userId: user.id,
    items: {
      create: [
        { productId: starterPlanId, quantity: 1 },
        { productId: proPlanId, quantity: 2 },
      ]
    }
  },
  include: { items: true }
});

Drizzle bietet kein Äquivalent für nestierte Erstellvorgänge. Stattdessen müssen Sie die Logik selbst innerhalb einer expliziten Transaktion schreiben:

import { db } from './db';
import { orders, orderItems } from './schema';

const newOrder = await db.transaction(async (tx) => {
  const [order] = await tx
    .insert(orders)
    .values({ userId: user.id })
    .returning();

  await tx.insert(orderItems).values([
    { orderId: order.id, productId: starterPlanId, quantity: 1 },
    { orderId: order.id, productId: proPlanId, quantity: 2 },
  ]);

  return order;
});

Das bedeutet deutlich mehr Code, der geschrieben und gewartet werden muss. Wenn Ihre Anwendung stark auf diesem Muster beruht, sammelt sich der zusätzliche Boilerplate schnell an. Prismas Version bleibt kürzer und drückt die Absicht direkter aus. Wenn nestierte Mutationen häufig in Ihrem Domainmodell vorkommen, ist es eine völlig vernünftige Entscheidung, bei Prisma zu bleiben.

Entscheiden, ob man wechseln soll

Bedenken Sie einen Umstieg auf Drizzle, wenn eine der folgenden Punkte zutrifft: Sie setzen Ihre Anwendungen auf serverlosen oder Edge-Plattformen ein, bei denen die Größe der Pakete sowie die Latenz beim ersten Start einen direkten Einfluss auf die Kosten oder die Benutzererfahrung haben; Sie schreiben häufig komplexe analytische SQL-Anfragen und möchten direkten Zugriff darauf haben; Ihr Team ist so gut in SQL, dass eine Abstraktionsschicht keinen nennenswerten Mehrwert bietet; oder Sie möchten Migrationsprozesse in Form einfacher SQL-Dateien darstellen, die jeder Entwickler unabhängig von seiner Erfahrung mit ORMs lesen und überprüfen kann.

Bleiben Sie bei Prisma, wenn: Sie an einem Prototypen oder einer internen Tool arbeiten, bei dem eine schnelle Bereitstellung wichtiger ist als das Abschneiden von Millisekunden aus der Laufzeit; Ihre Anwendung viele tief verschachtelte Beziehungsoperationen durchführt; ein Teil Ihres Teams mit rohem SQL weniger vertraut ist und vom deklarativen Modellierungsstil von Prisma profitiert; oder Sie bereits auf Prisma 7 umgestiegen sind, bei dem das historische Problem der großen Paketgröße deutlich weniger gravierend ist.

Auch die Timing-Faktoren sind hier wichtig. Prisma 7, das Ende 2025 veröffentlicht wurde, hat den Rust-Engine vollständig entfernt. Die aktuelle Größe des Pakets liegt bei etwa 1,6 MB – das ist immer noch mehr als 200 Mal so groß wie die Größe von Drizzle, aber bei weitem nicht so groß wie das ursprüngliche 14 MB große Binärdatei, das die Leute zu Alternativen veranlasste. Wenn die Größe des Pakets der einzige Grund für einen Wechsel war, lohnt es sich, zu prüfen, ob dieser Grund nach der erheblichen Verkleinerung von Prisma noch gilt.

Für das Team hinter dieser Migration lautete die Antwort weiterhin ja. Bessere Typisierungen für raw-Queries sowie das Fehlen eines Code-Generierungsschritts rechtfertigten den Wechsel bereits für sich allein, unabhängig von möglichen Größeneinsparungen. Nach sechs Monaten betrachtet hält die Entscheidung weiterhin stand. Allerdings ist das nicht die universell richtige Antwort – wer behauptet, ein bestimmtes ORM sei für jeden Anwendungsfall objektiv überlegen, spricht nicht die Wahrheit.

Nächste Schritte, falls Sie es selbst ausprobieren möchten

Falls Sie gerne mit Drizzle an einer bereits vorhandenen Datenbank experimentieren möchten, ohne sich zu einer vollständigen Neuschreibung verpflichten zu müssen, ermöglichen die Introspektionswerkzeuge des Drizzle Kits es drizzle-kit pull, Ihr vorhandenes Schema zu lesen und automatisch die entsprechenden TypeScript-Definitionen zu erzeugen. Dadurch erhalten Sie eine funktionierende Grundlage, anstatt jede Tabellendefinition von Grund auf selbst zu schreiben.

sql-Template-Operator sowie das vollständige Migrationskommando-Set. Es lohnt sich auch, Prismas eigene Vergleichsseite zu Drizzle zu lesen – sie bietet eine ausgewogene Darstellung der Vor- und Nachteile aus Prismas Sicht, was ehrlicher ist als die meisten Vergleiche anderer Anbieter.