TypeScript ORM

Drizzle ORM

Drizzle est un ORM TypeScript-first qui garde le SQL bien en vue. Définissez vos tables en TypeScript, composez des requêtes qui reflètent le SQL qu'elles génèrent, et exécutez le tout sur un runtime léger et optimisé pour l'edge.

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" }),
});
Sortie
2021
Licence
Apache-2.0
Langage
TypeScript
Runtime
Léger, sans moteur Rust ni codegen
Outil de migration
drizzle-kit
Bases de données
PostgreSQL, MySQL, SQLite et variantes edge

Pourquoi c'est important

Pourquoi les développeurs choisissent Drizzle

Des requêtes qui reflètent le SQL

select, from, where, join et groupBy s'alignent un pour un avec l'instruction que Drizzle génère, ainsi le code se lit comme la requête.

Un runtime minuscule

Il n'y a pas de moteur de requête binaire ni de client généré, donc une fonction serverless ne transporte que le constructeur de requêtes et son driver.

drizzle-kit pour les migrations

Générez des migrations SQL révisables, appliquez-les dans l'ordre, ou poussez un schéma directement vers une base de données temporaire lors du prototypage.

Le tableau complet

Les trois piliers de Drizzle

Votre schéma est en TypeScript, vos requêtes se lisent comme du SQL, et la base de données conserve toutes ses fonctionnalités natives.

Schéma en TypeScript

Définir

Les tables sont déclarées avec des constructeurs typés comme pgTable, et les méthodes de colonnes ajoutent les mêmes contraintes que vous écririez en DDL.

Requêtes au format SQL

Interroger

Le constructeur de requêtes compose des clauses plutôt que de les masquer, et un template sql couvre tout ce que le constructeur n'expose pas.

Votre base de données, inchangée

Connecter

Les fonctionnalités spécifiques aux dialectes restent disponibles au lieu d'être uniformisées, permettant à PostgreSQL, MySQL et SQLite de garder leurs propres forces.

Modèle de données

La table users, telle qu'elle est écrite

Chaque appel au constructeur correspond à une colonne et une contrainte réelles. La base de données voit des noms en snake_case tandis que le code conserve le camelCase.

La table users, telle qu'elle est écriteTable PostgreSQL
  • iduuidClé primaire ; defaultRandom() devient gen_random_uuid()
  • emailtextNOT NULL avec une contrainte UNIQUE
  • nametextNom d'affichage nullable
  • createdAttimestamptzNOT NULL, valeur par défaut now() en UTC

Chaque appel au constructeur correspond à une colonne et une contrainte réelles. La base de données voit des noms en snake_case tandis que le code conserve le camelCase.

Un bref aperçu

Bref historique d'un jeune ORM

  1. 2021

    Drizzle devient open-source

    Le projet commence comme un petit toolkit SQL TypeScript plutôt que comme un ORM complet.

    21
  2. 2022

    Arrivée de drizzle-kit

    Une CLI compagnon ajoute la génération de schéma, les migrations et l'introspection de base de données.

    22
  3. 2023

    Priorité au serverless et à l'edge

    Des drivers pour Neon, PlanetScale, Turso et Cloudflare D1 en font un choix naturel pour les runtimes edge.

    23
  4. 2024

    Maturation des requêtes relationnelles

    L'API db.query et le système de relations deviennent un moyen de premier choix pour charger des données imbriquées.

    24
  5. 2025

    Un standard pour les bases de données edge

    Drizzle devient un choix courant partout où un petit bundle et des démarrages à froid rapides sont essentiels.

    25

Le guide complet

Drizzle ORM: Tout ce que vous devez savoir

Qu’est-ce que Drizzle ?

Drizzle est un ORM TypeScript qui considère le SQL comme un citoyen de première classe. Votre schéma est du TypeScript classique, vos requêtes sont construites à partir de méthodes qui reflètent les clauses SQL, et le runtime est suffisamment léger pour s’exécuter sur l’edge. Il n’y a pas d’étape de génération de code ni de moteur binaire : vous importez drizzle-orm et vous effectuez vos requêtes.

Cette philosophie se ressent dans chaque API. db.select().from(users).where(eq(users.id, id)) se lit dans le même ordre que le SQL qu’il produit. La bibliothèque ne cache pas la base de données, elle la type. Si vous comprenez déjà les jointures, les index et ON CONFLICT, vous comprenez déjà l’essentiel de Drizzle.

Drizzle supporte PostgreSQL, MySQL et SQLite, ainsi que des drivers serverless et edge tels que Neon, PlanetScale, Turso, Cloudflare D1 et le SQLite intégré de Bun. Le même schéma et le même style de requête s’appliquent à tous, c’est pourquoi Drizzle est devenu un choix courant pour les bases de données edge et serverless.

Votre schéma est en TypeScript

Il n’y a pas de schema.prisma ni de client généré. Vous définissez vos tables à l’aide d’un builder par dialecte et vous les exportez.

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(),
});

Le premier argument est le nom de la table SQL, et chaque builder de colonne prend le nom de la colonne. Ce mappage explicite permet à la base de données d’utiliser snake_case tandis que votre code utilise camelCase. Les méthodes de colonne telles que .notNull(), .unique() et .default() ajoutent les mêmes contraintes que celles que vous écririez manuellement.

Les relations sont déclarées séparément avec relations(). Elles ne créent pas de colonnes ; elles indiquent à l’API de requête relationnelle comment les tables sont connectées.

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],
  }),
}));

Le typage est automatique

Drizzle infère les types de lignes à partir du schéma, vous n’avez donc presque jamais besoin de les écrire manuellement. Chaque table expose $inferSelect et $inferInsert :

import { users } from "./schema";

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

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

Les helpers InferSelectModel et InferInsertModel font la même chose et sont parfois plus lisibles selon les bases de code :

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

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

Comme ces types proviennent du même objet qui construit la requête, le renommage d’une colonne met à jour tous les consommateurs instantanément. C’est tout l’avantage de maintenir le schéma en TypeScript plutôt que dans un DSL séparé : il n’y a aucun fichier généré susceptible de se désynchroniser.

Les contraintes et les index sont définis à côté des colonnes

Les règles au niveau des colonnes couvrent NOT NULL, UNIQUE et les valeurs par défaut. Les règles au niveau de la table, telles que les clés composites, les index et les contraintes de vérification (checks), sont placées dans le troisième argument optionnel, qui renvoie un tableau.

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')`),
  ],
);

Le fait de conserver les index dans le schéma plutôt que dans un script exécuté manuellement signifie que drizzle-kit generate les crée en même temps que la table, et que chaque environnement les reçoit dans le même ordre. Si une requête est lente, l’index dont elle a besoin doit généralement être défini ici, à côté de la colonne concernée.

Connexion à une base de données

Créez une connexion via un driver, enveloppez-la avec drizzle, et passez le schéma pour que l’API relationnelle puisse détecter vos relations.

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 });

Remplacez node-postgres par le driver que vous déployez réellement : drizzle-orm/neon-http pour le point de terminaison HTTP de Neon, drizzle-orm/postgres-js, drizzle-orm/better-sqlite3, drizzle-orm/d1 ou drizzle-orm/bun-sqlite. L’API de requête utilisée ci-dessus db reste identique, et c’est là tout l’intérêt.

Drivers Serverless et Edge

La raison pour laquelle Drizzle s’adapte si bien aux environnements d’exécution edge est qu’il n’y a rien à packager en dehors du driver. Neon via HTTP et Cloudflare D1 sont les deux que vous rencontrerez le plus souvent.

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);
  },
};

Créez le client une seule fois par isolate plutôt qu’à chaque requête, et passez { schema } pour que db.query continue de fonctionner. Les drivers HTTP et D1 peuvent limiter les prepared statements ou les transactions selon le protocole ; vérifiez donc les notes du driver avant de vous appuyer sur l’un ou l’autre en production.

Le query builder se lit comme du SQL

Les requêtes sont assemblées en chaînant des méthodes qui correspondent aux clauses SQL. select, from, where, orderBy, limit et offset s’alignent ainsi un pour un avec l’instruction qu’ils génèrent.

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);

Les opérateurs sont des fonctions importées plutôt que des symboles : eq, ne, gt, gte, lt, lte, inArray, notInArray, like, ilike, isNull, isNotNull, ainsi que les combinateurs and, or et not. Lorsque vous avez besoin d’une fonctionnalité que le builder n’expose pas, sql vous permet de passer en SQL brut tout en restant paramétré.

import { sql } from "drizzle-orm";

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

Filtrage, tri et pagination

Les conditions se composent avec and, or et not, et les fonctions d’opérateurs couvrent les prédicats habituels. Les lire ensemble s’apparente à la lecture d’une clause WHERE.

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);

Il s’agit de la pagination par clés (keyset pagination) : au lieu d’utiliser offset, qui force la base de données à scanner et rejeter des lignes, vous paginez à partir du dernier id rencontré. Pour des ensembles de résultats plus restreints, limit et offset conviennent parfaitement, et pour les filtres basés sur des ensembles ou des valeurs nulles, le même builder s’applique :

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

Le tri utilise asc et desc sur les colonnes, ou sql pour des expressions telles que desc(sqlcount(${posts.id})). Comme chaque clause est explicite, il n’y a pas d’ordre caché ni de filtre par défaut pour vous surprendre.

Joins et agrégations

Les joins sont explicites, exactement comme en SQL. Vous choisissez innerJoin, leftJoin ou rightJoin et fournissez la condition.

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);

Comme vous écrivez vous-même le groupBy, il n’y a pas de plan de requête caché. Si le SQL est erroné, le TypeScript l’est aussi. Les sous-requêtes, les CTE avec with, les fonctions de fenêtrage (window functions) et les opérations d’ensemble sont toutes disponibles, soit via des helpers dédiés, soit via des fragments sql.

L’API de requêtes relationnelles

Le query builder est précis, mais s’avère verbeux pour le cas courant consistant à charger un utilisateur avec ses articles. L’API de requêtes relationnelles, db.query, se rapproche davantage de la syntaxe de 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 et findFirst exploitent le relations() que vous avez déclaré, et with les charge en un seul aller-retour par relation. Les deux API partagent le même schéma et peuvent être mélangées au sein d’un même projet : utilisez le query builder lorsque vous avez besoin de contrôle, et db.query lorsque vous souhaitez un résultat imbriqué sans avoir à écrire la jointure.

Écriture de données

Les insertions, mises à jour et suppressions sont tout aussi directes. Ajoutez .returning() pour récupérer les lignes affectées en une seule instruction, ce qui évite d’effectuer un SELECT supplémentaire.

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 et onConflictDoUpdate correspondent à ON CONFLICT pour les upserts. returning() est supporté sur PostgreSQL, SQLite et MariaDB ; MySQL ne le supporte pas, vous devrez donc effectuer une nouvelle sélection. L’API reflète délibérément le dialecte auquel vous êtes connecté plutôt que de prétendre que toutes les bases de données sont identiques.

Transactions

Les transactions sont des callbacks qui reçoivent un handle de transaction. Chaque requête effectuée à l’intérieur doit utiliser ce handle.

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));
});

Le fait de lever une exception (throw) à l’intérieur du callback annule la transaction (rollback). Les appels tx.transaction imbriqués deviennent des savepoints lorsque le dialecte les supporte, et vous pouvez définir un niveau d’isolation via l’objet d’options. Comme pour toute base de données, gardez le callback court et n’attendez pas (await) d’appels réseau sans rapport à l’intérieur.

Migrations avec drizzle-kit

drizzle-kit est la CLI compagnon. Elle lit drizzle.config.ts ainsi que le schéma, et gère les migrations.

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 compare le schéma avec le dernier snapshot, écrit un fichier SQL numéroté dans out, et enregistre un snapshot dans le journal. migrate applique les fichiers en attente dans l’ordre. Les deux sont sûrs pour la production car le SQL est un artefact révisable que vous committez.

push est le raccourci pour le prototypage : il aligne la base de données sur le schéma sans écrire de fichiers de migration. C’est pratique en développement, mais risqué en production pour les mêmes raisons que n’importe quelle commande de schema-push. pull fait l’inverse en introspectant une base de données existante vers TypeScript, et studio ouvre un navigateur de données local. Pour les drivers HTTP uniquement comme Neon, utilisez le migrateur du driver plutôt que la CLI :

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

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

Requêtes préparées (Prepared statements)

Drizzle peut préparer une requête une seule fois et l’exécuter plusieurs fois avec des paramètres différents, ce qui permet d’économiser les ressources liées à la planification sur les chemins critiques (hot paths).

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

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

Les placeholders sont déclarés avec sql.placeholder("name") et fournis lors de l’exécution. Les requêtes préparées sont nommées afin que le driver puisse mettre le plan en cache ; gardez à l’esprit que les poolers de connexion en mode transaction, comme PgBouncer, peuvent ne pas les préserver d’une connexion à l’autre.

Un seul ORM, plusieurs bases de données

Comme le query builder reflète le SQL, Drizzle s’adapte à chaque dialecte au lieu de les uniformiser. Vous importez les constructeurs de colonnes depuis drizzle-orm/pg-core, drizzle-orm/mysql-core ou drizzle-orm/sqlite-core, et les types ainsi que les fonctions disponibles s’adaptent à la base de données.

Cela signifie que les fonctionnalités spécifiques à PostgreSQL, telles que jsonb, les tableaux, pgEnum et les colonnes générées, sont traitées comme des citoyens de première classe, tandis que MySQL et SQLite disposent de leurs propres équivalents. La contrepartie est que le transfert d’un schéma entre différents dialectes demande un effort réel : les abstractions sont partagées, mais pas le SQL. C’est un compromis délibéré pour les équipes qui privilégient les capacités réelles de leur base de données plutôt qu’un plus petit dénominateur commun.

Pourquoi on a l’impression d’écrire du SQL

La plupart des ORM masquent la base de données pour exposer des objets. Drizzle fait l’inverse : il vous apporte des types autour de la base de données que vous connaissez déjà. Cela entraîne trois conséquences.

Premièrement, le débogage est plus simple. La requête que vous écrivez est celle qui est exécutée ; ainsi, la lecture d’un log de requête lente ou d’une sortie EXPLAIN renvoie directement au code. Deuxièmement, l’apprentissage est transférable, car vos connaissances sur les jointures, les index et ON CONFLICT s’appliquent sans changement. Troisièmement, il y a moins de “magie” pour vous surprendre : pas de proxys de lazy-loading qui déclenchent des requêtes dès que vous accédez à une propriété, et pas d’identity map pour décider de ce qui est déjà en mémoire.

Le revers de la médaille est qu’il est attendu que vous maîtrisiez le SQL. Drizzle générera volontiers une requête inefficace si vous lui en demandez une, et il ne vous sauvera pas d’un index manquant.

Performance et taille du bundle

Drizzle n’embarque aucun moteur Rust ni aucun client généré. Le runtime est du TypeScript pur compatible avec le tree-shaking ; ainsi, une fonction serverless ou un edge worker ne transporte que le query builder et le driver. Il n’y a aucun binaire séparé à inclure dans le bundle ou à démarrer à froid (cold-start), ce qui explique pourquoi les plateformes edge le privilégient.

Côté serveur, le principe est le même que pour tout ORM : c’est la base de données qui effectue le travail, et la structure de vos requêtes détermine la performance. Utilisez .returning() pour éviter les allers-retours superflus, préparez vos requêtes fréquentes, sélectionnez uniquement les colonnes nécessaires et laissez les index faire leur travail. Comme rien n’est mis en cache ou regroupé en arrière-plan, la performance que vous mesurez est celle du SQL que vous avez écrit.

Bonnes pratiques

  • Considérez schema.ts comme l’unique source de vérité et exportez les tables et les relations depuis un seul endroit.
  • Utilisez drizzle-kit generate et commitez le SQL ; ne laissez jamais push toucher la production.
  • Sélectionnez uniquement les colonnes dont vous avez besoin, et ajoutez .returning() plutôt que d’effectuer une lecture supplémentaire.
  • Privilégiez db.query avec with pour les lectures imbriquées et le query builder pour tout besoin personnalisé.
  • Enveloppez les écritures en plusieurs étapes dans des db.transaction et utilisez le handle tx tout au long du processus.
  • Utilisez des placeholders sql et des requêtes préparées (prepared statements) sur les chemins critiques.
  • Adaptez le driver à la plateforme et partagez une seule instance db par processus.
  • Ajoutez les index dans le schéma à côté des colonnes qu’ils couvrent.
  • Exécutez les migrations dans la CI/CD avant le déploiement du nouveau code.

Erreurs courantes

  • Appeler drizzle-kit push sur une base de données de production et supprimer des colonnes.
  • Utiliser le db externe à l’intérieur d’une transaction au lieu du tx fourni.
  • Oublier de passer { schema } à drizzle, ce qui casse db.query.
  • Écrire des conditions where avec le mauvais opérateur, comme eq alors que inArray est nécessaire.
  • Sélectionner toutes les colonnes par défaut dans les chemins critiques (hot paths).
  • Omettre les index sur les clés étrangères utilisées par les jointures.
  • Supposer que MySQL supporte .returning().
  • Considérer les fragments sql comme étant automatiquement sécurisés lorsqu’ils interpolent des chaînes de caractères utilisateur.
  • Oublier de lancer generate après avoir modifié le schéma, puis se demander pourquoi la migration est vide.

Et après ?

Drizzle valorise la connaissance de la base de données sous-jacente ; nous vous conseillons donc de lire le guide PostgreSQL pour en savoir plus sur les index, les transactions et la planification des requêtes. Vous pouvez également le comparer à Prisma, qui génère un client à partir d’un schéma, ou à TypeORM, qui modélise les entités sous forme de classes. Si vous apprenez encore le langage lui-même, le guide TypeScript constitue le socle sur lequel reposent ces trois outils.

En pratique

Schéma, jointure, requête relationnelle, transaction

Quatre onglets couvrant le schéma, les deux styles de requête et une écriture atomique.

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],
  }),
}));

Requête format SQL vs requête objet

Drizzle garde les clauses visibles et retourne des lignes typées ; Prisma masque le SQL derrière un objet et génère un client. Les deux sont valides, mais un seul permet de lire la requête comme du SQL.

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 vs push

generate écrit du SQL versionné que vous pouvez réviser et rejouer. push aligne la base de données sur le schéma sans historique, ce qui convient pour un prototype mais est risqué en production.

Préférer
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
Éviter
# No files, no journal, no audit trail.
pnpm drizzle-kit push

Compromis

Drizzle est-il l'ORM adapté ?

Drizzle troque l'accompagnement guidé contre le contrôle. Cela convient aux équipes qui maîtrisent le SQL et privilégient un runtime léger, mais demande plus d'efforts aux autres.

Strengths

  • Vous connaissez toujours le SQL

    Les requêtes correspondent aux clauses, donc un log lent ou un résultat EXPLAIN pointe directement vers le code qui l'a produit.

  • Presque aucun overhead de runtime

    L'absence de moteur Rust et de génération de code signifie des bundles réduits, des démarrages à froid rapides et un déploiement serein sur les runtimes edge.

  • Des types sans étape de build

    Les types du schéma et des requêtes sont inférés depuis du TypeScript standard, il n'y a donc pas de commande generate à oublier dans la CI.

Trade-offs

  • Plus de connaissances SQL requises

    La bibliothèque ne vous empêchera pas d'écrire une requête inefficace. Les jointures, index et la gestion des conflits dépendent de votre expertise.

  • Relations configurées manuellement

    Vous déclarez les relations séparément et choisissez entre le constructeur de requêtes et db.query, ce qui est plus explicite mais demande plus de configuration.

  • Un écosystème plus jeune

    Il y a moins de plugins tiers et de documentation longue que pour les ORM plus anciens, bien que le cœur soit stable et bien documenté.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre Drizzle ORM ?

Notre tutoriel interactif vous guide à travers Drizzle ORM pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.