TypeScript ORM

Drizzle ORM

Drizzle ist ein TypeScript-first ORM, das SQL im Vordergrund behält. Definieren Sie Tabellen in TypeScript, erstellen Sie Abfragen, die das generierte SQL widerspiegeln, und lassen Sie alles auf einer schlanken, edge-freundlichen Runtime laufen.

intermediate14 min readUpdated 16. Sept. 2026
src/db/schema.ts
ts
// src/db/schema.ts
import { pgTable, text, uuid, timestamp } from "drizzle-orm/pg-core";

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

export const posts = pgTable("posts", {
  id: uuid("id").primaryKey().defaultRandom(),
  title: text("title").notNull(),
  authorId: uuid("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
});
Veröffentlicht
2021
Lizenz
Apache-2.0
Sprache
TypeScript
Runtime
Schlank, keine Rust-Engine oder Codegen
Migrationstool
drizzle-kit
Datenbanken
PostgreSQL, MySQL, SQLite und Edge-Varianten

Warum es wichtig ist

Warum Entwickler Drizzle wählen

Abfragen, die SQL widerspiegeln

select, from, where, join und groupBy stehen eins-zu-eins mit dem Statement in Verbindung, das Drizzle generiert, sodass der Code wie die Abfrage gelesen wird.

Eine winzige Runtime

Es gibt keine binäre Query-Engine und keinen generierten Client, sodass eine serverless Funktion nur den Query Builder und seinen Treiber mitführt.

drizzle-kit für Migrationen

Generieren Sie überprüfbare SQL-Migrationen, wenden Sie diese in der richtigen Reihenfolge an oder pushen Sie ein Schema beim Prototyping direkt in eine temporäre Datenbank.

Das Gesamtbild

Drei Grundideen hinter Drizzle

Ihr Schema ist TypeScript, Ihre Abfragen lesen sich wie SQL und die Datenbank behält jedes Feature, mit dem sie ausgeliefert wurde.

Schema in TypeScript

Definieren

Tabellen werden mit typisierten Buildern wie pgTable deklariert, und Spaltenmethoden fügen dieselben Constraints hinzu, die Sie in DDL schreiben würden.

SQL-förmige Abfragen

Abfragen

Der Query Builder setzt Klauseln zusammen, anstatt sie zu verstecken, und ein sql-Template deckt alles ab, was der Builder nicht anbietet.

Ihre Datenbank, unverändert

Verbinden

Dialektspezifische Features bleiben verfügbar, anstatt vereinfacht zu werden, sodass PostgreSQL, MySQL und SQLite jeweils ihre eigenen Stärken behalten.

Datenmodell

Die Users-Tabelle, exakt so geschrieben

Jeder Builder-Aufruf bildet eine echte Spalte und ein Constraint ab. Die Datenbank sieht snake_case-Namen, während der Code camelCase beibehält.

Die Users-Tabelle, exakt so geschriebenPostgreSQL Tabelle
  • iduuidPrimärschlüssel; defaultRandom() wird zu gen_random_uuid()
  • emailtextNOT NULL mit einem UNIQUE-Constraint
  • nametextOptionaler Anzeigename (Nullable)
  • createdAttimestamptzNOT NULL, Standardwert now() in UTC

Jeder Builder-Aufruf bildet eine echte Spalte und ein Constraint ab. Die Datenbank sieht snake_case-Namen, während der Code camelCase beibehält.

Eine kurze Geschichte

Eine kurze Geschichte eines jungen ORM

  1. 2021

    Drizzle wird Open Source

    Das Projekt beginnt als kleines TypeScript SQL-Toolkit anstatt als vollwertiges ORM.

    21
  2. 2022

    drizzle-kit erscheint

    Ein begleitendes CLI fügt Schema-Generierung, Migrationen und Datenbank-Introspektion hinzu.

    22
  3. 2023

    Serverless und Edge First

    Treiber für Neon, PlanetScale, Turso und Cloudflare D1 machen es zu einer natürlichen Wahl für Edge-Runtimes.

    23
  4. 2024

    Relationale Abfragen reifen

    Die db.query API und das Relations-System entwickeln sich zu einem erstklassigen Weg, verschachtelte Daten zu laden.

    24
  5. 2025

    Ein Standard für Edge-Datenbanken

    Drizzle wird zu einer gängigen Wahl, wo ein kleines Bundle und schnelle Cold Starts entscheidend sind.

    25

Der vollständige Leitfaden

Drizzle ORM: Alles was Sie wissen müssen

Was ist Drizzle?

Drizzle ist ein TypeScript ORM, das SQL als First-Class Citizen behandelt. Ihr Schema besteht aus gewöhnlichem TypeScript, Ihre Queries werden über Methoden aufgebaut, die SQL-Klauseln widerspiegeln, und die Runtime ist schlank genug, um auf der Edge zu laufen. Es gibt keinen Code-Generation-Schritt und keine Binary Engine: Sie importieren drizzle-orm und führen Queries aus.

Dieses Design spiegelt sich in jeder API wider. db.select().from(users).where(eq(users.id, id)) wird in derselben Reihenfolge gelesen wie das SQL, das daraus resultiert. Die Library versteckt die Datenbank nicht, sondern versieht sie mit Typen. Wenn Sie Joins, Indexes und ON CONFLICT bereits verstehen, verstehen Sie bereits den Großteil von Drizzle.

Drizzle unterstützt PostgreSQL, MySQL und SQLite sowie Serverless- und Edge-Driver wie Neon, PlanetScale, Turso, Cloudflare D1 und das integrierte SQLite von Bun. Das gleiche Schema und der gleiche Query-Stil gelten für alle diese Datenbanken, weshalb Drizzle zu einer beliebten Wahl für Edge- und Serverless-Datenbanken geworden ist.

Dein Schema ist TypeScript

Es gibt kein schema.prisma und keinen generierten Client. Du definierst Tabellen mit einem Builder pro Dialekt und exportierst diese.

import { pgTable, text, uuid, boolean, timestamp, integer } from "drizzle-orm/pg-core";

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

export const posts = pgTable("posts", {
  id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
  title: text("title").notNull(),
  published: boolean("published").notNull().default(false),
  authorId: uuid("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});

Das erste Argument ist der SQL-Tabellenname, und jeder Column-Builder nimmt den Spaltennamen entgegen. Dieses explizite Mapping ermöglicht es der Datenbank, snake_case zu verwenden, während dein Code camelCase nutzt. Column-Methoden wie .notNull(), .unique() und .default() fügen dieselben Constraints hinzu, die du sonst manuell schreiben würdest.

Relationen werden separat mit relations() deklariert. Sie erstellen keine Spalten, sondern teilen der relationalen Query-API mit, wie die Tabellen miteinander verknüpft sind.

import { relations } from "drizzle-orm";

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

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

Typen gibt es gratis

Drizzle leitet die Zeilentypen aus dem Schema ab, sodass man sie selten manuell schreiben muss. Jede Tabelle stellt $inferSelect und $inferInsert bereit:

import { users } from "./schema";

type User = typeof users.$inferSelect;
type NewUser = typeof users.$inferInsert;

function displayName(user: User) {
  return user.name ?? user.email;
}

Die Helper InferSelectModel und InferInsertModel bewirken dasselbe und lassen sich in einigen Codebases besser lesen:

import type { InferInsertModel, InferSelectModel } from "drizzle-orm";

type User = InferSelectModel<typeof users>;
type NewUser = InferInsertModel<typeof users>;

Da diese Typen aus demselben Objekt stammen, das die Query aufbaut, aktualisiert eine Umbenennung einer Spalte sofort alle Konsumenten. Das ist der Vorteil davon, das Schema in TypeScript statt in einer separaten DSL zu führen: Es gibt keine generierte Datei, die asynchron werden oder aus dem Sync geraten kann.

Constraints und Indizes direkt bei den Spalten

Regeln auf Spaltenebene decken NOT NULL, UNIQUE und Standardwerte ab. Regeln auf Tabellenebene, wie zusammengesetzte Schlüssel, Indizes und Checks, kommen in das optionale dritte Argument, welches ein Array zurückgibt.

import { sql } from "drizzle-orm";
import {
  check,
  index,
  pgTable,
  primaryKey,
  text,
  uniqueIndex,
  uuid,
} from "drizzle-orm/pg-core";

export const memberships = pgTable(
  "memberships",
  {
    userId: uuid("user_id").notNull().references(() => users.id),
    orgId: uuid("org_id").notNull(),
    role: text("role").notNull().default("member"),
  },
  (table) => [
    primaryKey({ columns: [table.userId, table.orgId] }),
    index("memberships_org_idx").on(table.orgId),
    uniqueIndex("memberships_user_org_idx").on(table.userId, table.orgId),
    check("memberships_role_check", sql`${table.role} in ('member', 'admin')`),
  ],
);

Indizes im Schema statt in einem manuell ausgeführten Skript zu definieren, bedeutet, dass drizzle-kit generate diese zusammen mit der Tabelle erstellt und jede Umgebung sie in der gleichen Reihenfolge erhält. Wenn eine Abfrage langsam ist, gehört der benötigte Index normalerweise hier direkt neben die Spalte, die er abdeckt.

Verbindung zur Datenbank herstellen

Erstellen Sie eine Driver-Verbindung, umschließen Sie diese mit drizzle und übergeben Sie das Schema, damit die relationale API Ihre Relationen erkennen kann.

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export const db = drizzle(pool, { schema });

Ersetzen Sie node-postgres durch den Driver, den Sie tatsächlich einsetzen: drizzle-orm/neon-http für den HTTP-Endpunkt von Neon, drizzle-orm/postgres-js, drizzle-orm/better-sqlite3, drizzle-orm/d1 oder drizzle-orm/bun-sqlite. Die darüber liegende Query-API db bleibt identisch, was genau das Ziel ist.

Serverless- und Edge-Driver

Der Grund, warum Drizzle so gut mit Edge-Runtimes funktioniert, ist, dass außer dem Driver nichts gebündelt werden muss. Neon über HTTP und Cloudflare D1 sind die beiden Driver, denen man am häufigsten begegnet.

import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);

export const db = drizzle(sql, { schema });
import { drizzle } from "drizzle-orm/d1";

export default {
  async fetch(_request: Request, env: Env) {
    const db = drizzle(env.DB);
    const rows = await db.select().from(users);
    return Response.json(rows);
  },
};

Erstellen Sie den Client einmal pro Isolate anstatt pro Request und übergeben Sie { schema }, damit db.query weiterhin funktioniert. HTTP- und D1-Driver können Prepared Statements oder Transaktionen je nach Protokoll einschränken. Prüfen Sie daher die Driver-Notizen, bevor Sie sich in der Produktion auf eines von beiden verlassen.

Der Query Builder liest sich wie SQL

Queries werden durch das Verketten von Methoden zusammengesetzt, die SQL-Klauseln entsprechen. select, from, where, orderBy, limit und offset stehen eins zu eins für das Statement, das sie generieren.

import { eq, desc, ilike, and } from "drizzle-orm";

const rows = await db
  .select({
    id: posts.id,
    title: posts.title,
    publishedAt: posts.createdAt,
  })
  .from(posts)
  .where(and(eq(posts.published, true), ilike(posts.title, "%drizzle%")))
  .orderBy(desc(posts.createdAt))
  .limit(20);

Operatoren sind importierte Funktionen statt Symbole: eq, ne, gt, gte, lt, lte, inArray, notInArray, like, ilike, isNull, isNotNull sowie die Kombinatoren and, or und not. Wenn Sie etwas benötigen, das der Builder nicht anbietet, ermöglicht sql den Wechsel zu raw SQL, während die Parameterisierung beibehalten wird.

import { sql } from "drizzle-orm";

const active = await db
  .select()
  .from(users)
  .where(sql`${users.createdAt} > now() - interval '30 days'`);

Filtern, Sortieren und Pagination

Bedingungen werden mit and, or und not kombiniert, und die Operator-Funktionen decken die üblichen Prädikate ab. Das Zusammenspiel liest sich fast wie eine WHERE-Klausel.

import { and, asc, gt, inArray, isNull, or } from "drizzle-orm";

const page = await db
  .select({ id: posts.id, title: posts.title, createdAt: posts.createdAt })
  .from(posts)
  .where(and(eq(posts.published, true), gt(posts.id, cursor)))
  .orderBy(asc(posts.id))
  .limit(20);

Das ist Keyset-Pagination: Anstatt offset zu verwenden, was die Datenbank dazu zwingt, Zeilen zu scannen und zu verwerfen, paginieren Sie ab der letzten ID, die Sie gesehen haben. Für kleinere Ergebnismengen sind limit und offset völlig ausreichend, und für nullable oder set-basierte Filter gilt derselbe Builder:

const results = await db
  .select()
  .from(users)
  .where(
    or(
      isNull(users.name),
      inArray(users.email, ["[email protected]", "[email protected]"]),
    ),
  );

Für das Sortieren werden asc und desc auf Spalten angewendet oder sql für Ausdrücke wie desc(sqlcount(${posts.id})). Da jede Klausel explizit ist, gibt es keine versteckte Sortierung oder Standardfilter, die Sie überraschen könnten.

Joins und Aggregate

Joins sind explizit, genau wie in SQL. Sie wählen innerJoin, leftJoin oder rightJoin und geben die Bedingung an.

const report = await db
  .select({
    id: users.id,
    email: users.email,
    postCount: sql<number>`count(${posts.id})`.mapWith(Number),
  })
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .groupBy(users.id)
  .orderBy(desc(sql`count(${posts.id})`))
  .limit(20);

Da Sie den groupBy selbst schreiben, gibt es keinen versteckten Query-Plan. Wenn das SQL falsch wäre, wäre auch das TypeScript falsch. Subqueries, CTEs mit with, Window-Funktionen und Set-Operationen sind alle verfügbar, entweder über dedizierte Helper oder über sql-Fragmente.

Die Relational Query API

Der Query Builder ist präzise, aber für den häufigen Anwendungsfall – das Laden eines Benutzers zusammen mit seinen Posts – sehr ausführlich. Die Relational Query API, db.query, liest sich eher wie Prisma:

const result = await db.query.users.findMany({
  columns: { id: true, email: true },
  with: {
    posts: {
      columns: { id: true, title: true },
      where: (posts, { eq }) => eq(posts.published, true),
      orderBy: (posts, { desc }) => [desc(posts.createdAt)],
      limit: 10,
    },
  },
});

db.query.users.findMany und findFirst erkennen das relations(), das Sie definiert haben, und with lädt diese in einem einzigen Roundtrip pro Relation. Die beiden APIs nutzen dasselbe Schema und können innerhalb einer Codebasis gemischt werden: Verwenden Sie den Query Builder, wenn Sie volle Kontrolle benötigen, und db.query, wenn Sie ein verschachteltes Ergebnis wünschen, ohne den Join manuell schreiben zu müssen.

Daten schreiben

Inserts, Updates und Deletes funktionieren genauso direkt. Füge .returning() hinzu, um die betroffenen Zeilen in einem einzigen Statement zurückzuerhalten, wodurch ein nachfolgendes SELECT vermieden wird.

await db
  .insert(users)
  .values({ email: "[email protected]", name: "Ada" })
  .returning();

await db
  .insert(posts)
  .values([
    { title: "First", authorId },
    { title: "Second", authorId, published: true },
  ])
  .onConflictDoNothing({ target: posts.id });

await db
  .update(users)
  .set({ name: "Ada Lovelace" })
  .where(eq(users.id, id))
  .returning();

await db.delete(posts).where(eq(posts.id, postId));

onConflictDoNothing und onConflictDoUpdate werden bei Upserts auf ON CONFLICT abgebildet. returning() wird von PostgreSQL, SQLite und MariaDB unterstützt; MySQL unterstützt dies nicht, weshalb dort ein erneuter Select nötig ist. Die API spiegelt bewusst den Dialekt der verbundenen Datenbank wider, anstatt vorzugeben, dass alle Datenbanken identisch seien.

Transaktionen

Transaktionen sind Callbacks, die einen Transaction-Handle erhalten. Jede Abfrage innerhalb des Callbacks muss diesen Handle verwenden.

await db.transaction(async (tx) => {
  await tx
    .update(accounts)
    .set({ balance: sql`${accounts.balance} - 5000` })
    .where(eq(accounts.id, from));

  await tx
    .update(accounts)
    .set({ balance: sql`${accounts.balance} + 5000` })
    .where(eq(accounts.id, to));
});

Ein Throw innerhalb des Callbacks führt zu einem Rollback der Transaktion. Verschachtelte tx.transaction-Aufrufe werden zu Savepoints, sofern der Dialekt diese unterstützt. Über das Options-Objekt kannst du zudem das Isolation Level festlegen. Wie bei jeder Datenbank sollte der Callback kurz gehalten werden; vermeide es, darin nicht zusammenhängende Netzwerkaufrufe mit await zu erwarten.

Migrationen mit drizzle-kit

drizzle-kit ist die dazugehörige CLI. Sie liest drizzle.config.ts sowie das Schema aus und verwaltet die Migrationen.

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL! },
});
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
pnpm drizzle-kit push
pnpm drizzle-kit studio

generate vergleicht das Schema mit dem letzten Snapshot, schreibt eine nummerierte SQL-Datei in out und zeichnet einen Snapshot im Journal auf. migrate wendet ausstehende Dateien in der richtigen Reihenfolge an. Beides ist für die Produktion sicher, da das SQL ein überprüfbares Artefakt ist, das ihr committet.

push ist die Abkürzung für das Prototyping: Damit wird die Datenbank an das Schema angepasst, ohne Migrationsdateien zu schreiben. Das ist in der Entwicklung praktisch, aber in der Produktion aus denselben Gründen riskant wie jeder andere schema-push Befehl. pull funktioniert in die andere Richtung und führt eine Introspektion einer bestehenden Datenbank in TypeScript durch, während studio einen lokalen Daten-Browser öffnet. Bei HTTP-only Treibern wie Neon verwendet bitte den Migrator des Treibers anstelle der CLI:

import { migrate } from "drizzle-orm/neon-http/migrator";

await migrate(db, { migrationsFolder: "./drizzle" });

Prepared Statements

Drizzle kann eine Query einmal vorbereiten und sie mehrfach mit unterschiedlichen Parametern ausführen, was den Planning-Overhead in Performance-kritischen Bereichen (hot paths) reduziert.

const getUser = db
  .select()
  .from(users)
  .where(eq(users.id, sql.placeholder("id")))
  .prepare("get_user");

const user = await getUser.execute({ id: userId });

Platzhalter werden mit sql.placeholder("name") deklariert und bei der Ausführung übergeben. Prepared Statements werden benannt, damit der Driver den Plan cachen kann; beachte jedoch, dass Connection Pooler im Transaction-Mode, wie etwa PgBouncer, diese möglicherweise nicht über verschiedene Verbindungen hinweg beibehalten.

Ein ORM, viele Datenbanken

Da der Query Builder SQL widerspiegelt, passt sich Drizzle an jeden Dialekt an, anstatt diese zu vereinheitlichen. Sie importieren Column Builder aus drizzle-orm/pg-core, drizzle-orm/mysql-core oder drizzle-orm/sqlite-core, und die verfügbaren Typen und Funktionen richten sich nach der jeweiligen Datenbank.

Das bedeutet, dass PostgreSQL-spezifische Features wie jsonb, Arrays, pgEnum und Generated Columns erstklassig unterstützt werden, während MySQL und SQLite ihre eigenen Äquivalente erhalten. Der Preis dafür ist, dass die Migration eines Schemas zwischen Dialekten mit Aufwand verbunden ist: Die Abstraktionen sind zwar identisch, das SQL jedoch nicht. Dies ist ein bewusster Kompromiss für Teams, die die tatsächlichen Funktionen der Datenbank nutzen möchten, anstatt sich auf den kleinsten gemeinsamen Nenner zu beschränken.

Warum es sich wie SQL anfühlt

Die meisten ORMs verstecken die Datenbank und stellen stattdessen Objekte bereit. Drizzle macht genau das Gegenteil: Es liefert dir Typen für die Datenbank, die du bereits kennst. Daraus ergeben sich drei Konsequenzen.

Erstens ist das Debugging einfacher. Die Abfrage, die du geschrieben hast, ist genau die Abfrage, die ausgeführt wird. Das Lesen eines Slow-Logs oder einer EXPLAIN-Ausgabe lässt sich also direkt auf den Code zurückführen. Zweitens ist das Wissen übertragbar, da Kenntnisse über Joins, Indizes und ON CONFLICT unverändert anwendbar sind. Drittens gibt es weniger „Magie“, die dich überraschen könnte: Keine Lazy-Loading-Proxies, die Abfragen auslösen, sobald du eine Eigenschaft ansprichst, und keine Identity Map, die entscheidet, was sich bereits im Speicher befindet.

Der Preis dafür ist, dass du SQL beherrschen solltest. Drizzle generiert gerne eine ineffiziente Abfrage, wenn du danach fragst, und es wird dich nicht vor einem fehlenden Index bewahren.

Performance und Bundle-Größe

Drizzle wird ohne Rust-Engine und ohne generierten Client ausgeliefert. Die Runtime besteht aus einfachem TypeScript, das Tree-Shaking unterstützt. Somit enthält eine Serverless Function oder ein Edge Worker nur den Query Builder und den Driver. Es gibt keine separate Binary, die gebündelt werden muss oder Cold-Starts verursacht, was ein wesentlicher Grund ist, warum Edge-Plattformen Drizzle bevorzugen.

Auf dem Server verhält es sich wie bei jedem ORM: Die Datenbank erledigt die Arbeit, und die Struktur Ihrer Queries entscheidet über die Performance. Nutzen Sie .returning(), um zusätzliche Round-Trips zu vermeiden, bereiten Sie häufig genutzte Queries vor, wählen Sie nur die Spalten aus, die Sie tatsächlich benötigen, und lassen Sie Indexe ihre Arbeit machen. Da im Hintergrund nichts ohne Ihr Wissen gecacht oder gebatcht wird, entspricht die gemessene Performance exakt der Performance des von Ihnen geschriebenen SQL.

Best Practices

  • Behandeln Sie schema.ts als die einzige „Source of Truth“ und exportieren Sie Tabellen und Relationen von einem zentralen Ort aus.
  • Nutzen Sie drizzle-kit generate und committen Sie das SQL; lassen Sie push niemals die Production-Umgebung berühren.
  • Wählen Sie nur die Spalten aus, die Sie tatsächlich benötigen, und fügen Sie .returning() hinzu, anstatt einen weiteren Read-Vorgang durchzuführen.
  • Bevorzugen Sie db.query mit with für verschachtelte Reads und den Query Builder für alle benutzerdefinierten Abfragen.
  • Kapseln Sie mehrstufige Schreibvorgänge in db.transaction ein und verwenden Sie konsistent den tx-Handle.
  • Nutzen Sie sql-Platzhalter und Prepared Statements in performance-kritischen Bereichen („hot paths“).
  • Passen Sie den Driver an die Plattform an und teilen Sie sich eine einzige db-Instanz pro Prozess.
  • Fügen Sie Indexe im Schema direkt neben den Spalten hinzu, die sie abdecken.
  • Führen Sie Migrationen in der CI/CD-Pipeline aus, bevor der neue Code deployed wird.

Häufige Fehler

  • drizzle-kit push gegen eine Produktionsdatenbank auszuführen und dabei Spalten zu löschen.
  • Das äußere db innerhalb einer Transaktion zu verwenden, anstatt des bereitgestellten tx.
  • Zu vergessen, { schema } an drizzle zu übergeben, was db.query beeinträchtigt.
  • where-Bedingungen mit dem falschen Operator zu schreiben, zum Beispiel eq, wo inArray benötigt wird.
  • In Performance-kritischen Bereichen (Hot Paths) standardmäßig alle Spalten auszuwählen.
  • Indizes bei Fremdschlüsseln zu vergessen, die in Joins verwendet werden.
  • Davon auszugehen, dass MySQL .returning() unterstützt.
  • sql-Fragmente als automatisch sicher zu betrachten, wenn sie User-Strings interpolieren.
  • Zu vergessen, generate nach der Bearbeitung des Schemas auszuführen, und sich dann zu fragen, warum die Migration leer ist.

Wie geht es weiter?

Drizzle belohnt fundierte Kenntnisse über die zugrunde liegende Datenbank. Lesen Sie daher den PostgreSQL-Guide zu Indizes, Transaktionen und Query Planning. Vergleichen Sie Drizzle mit Prisma, das einen Client aus einem Schema generiert, und TypeORM, das Entities als Klassen modelliert. Falls Sie sich noch in die Sprache einarbeiten, ist der TypeScript-Guide das Fundament, auf dem alle drei aufbauen.

In der Praxis

Schema, Join, relationale Abfrage, Transaktion

Vier Tabs, die das Schema, beide Abfragestile und einen atomaren Schreibvorgang abdecken.

src/db/schema.ts
import { relations } from "drizzle-orm";
import { pgTable, text, uuid, boolean, timestamp } from "drizzle-orm/pg-core";

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

export const posts = pgTable("posts", {
  id: uuid("id").primaryKey().defaultRandom(),
  title: text("title").notNull(),
  published: boolean("published").notNull().default(false),
  authorId: uuid("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
});

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

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

SQL-förmige Abfrage gegenüber Objekt-Abfrage

Drizzle hält die Klauseln sichtbar und gibt typisierte Zeilen zurück; Prisma versteckt das SQL hinter einem Objekt und generiert einen Client. Beides ist valide, aber nur eines erlaubt es, die Abfrage als SQL zu lesen.

Drizzle
const rows = await db
  .select({ id: users.id, email: users.email })
  .from(users)
  .where(eq(users.email, email))
  .limit(1);
Prisma
const rows = await prisma.user.findMany({
  where: { email },
  select: { id: true, email: true },
  take: 1,
});

Generate gegenüber Push

generate schreibt versioniertes SQL, das Sie prüfen und erneut ausführen können. push bringt die Datenbank mit dem Schema in Einklang ohne Historie, was für Prototypen okay, aber für die Produktion unsicher ist.

Bevorzugt
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
Vermeiden
# No files, no journal, no audit trail.
pnpm drizzle-kit push

Abwägungen

Ist Drizzle das richtige ORM?

Drizzle tauscht Führungshilfe gegen Kontrolle. Das passt zu Teams, die SQL beherrschen und eine kleine Runtime schätzen, und stellt höhere Anforderungen an alle anderen.

Strengths

  • Sie kennen immer das SQL

    Abfragen bilden Klauseln ab, sodass ein langsames Log oder eine EXPLAIN-Ausgabe direkt auf den Code zeigt, der sie erzeugt hat.

  • Fast kein Runtime-Overhead

    Keine Rust-Engine und keine Codegenerierung bedeuten kleine Bundles, schnelle Cold Starts und eine komfortable Bereitstellung auf Edge-Runtimes.

  • Typen ohne Build-Schritt

    Schema- und Abfragetypen werden aus gewöhnlichem TypeScript abgeleitet, sodass es keinen generate-Befehl gibt, den man in der CI vergessen kann.

Trade-offs

  • Mehr SQL-Wissen erforderlich

    Die Library wird Sie nicht daran hindern, eine ineffiziente Abfrage zu schreiben. Joins, Indizes und Conflict-Handling liegen in Ihrer Verantwortung.

  • Relationen werden manuell verdrahtet

    Sie deklarieren Relationen separat und wählen zwischen dem Query Builder und db.query, was expliziter ist, aber mehr Setup erfordert.

  • Ein jüngeres Ökosystem

    Es gibt weniger Drittanbieter-Plugins und weniger ausführliches Material als für ältere ORMs, obwohl der Kern stabil und gut dokumentiert ist.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Drizzle ORM zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch Drizzle ORM — mit Quizzen und echtem Code, den Sie im Browser ausführen können.