Query Builder

Knex.js

Knex.js ist ein SQL Query Builder, kein vollständiges ORM. Er setzt Abfragen als JavaScript zusammen, bindet jeden Wert sicher, spricht mehrere Dialekte und liefert Migrationen sowie Seeds zusammen mit den Abfragen aus.

beginner14 min readUpdated 16. Sept. 2026
knexfile.ts
ts
// knexfile.ts
import type { Knex } from "knex";

const config: { [key: string]: Knex.Config } = {
  development: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    migrations: { directory: "./migrations" },
    seeds: { directory: "./seeds" },
    pool: { min: 2, max: 10 },
  },
  production: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    pool: { min: 2, max: 10 },
  },
};

export default config;
Veröffentlicht
2013
Art
SQL query builder
Dialekte
Postgres, MySQL, SQLite, MSSQL, Oracle
Sprache
JavaScript / TypeScript
Migrationen
Integrierter Schema Builder
Aktuelle Version
3.x

Warum es wichtig ist

Was ein Query Builder bietet

Komponierbares SQL

Kette select, where, join und orderBy, um ein Statement Stück für Stück aufzubauen, und nutze dann await, um die Abfrage auszuführen.

Schema Builder und Migrationen

Erstelle und ändere Tabellen in JavaScript, versioniere jede Änderung als Migration und befülle lokale Daten mit demselben Tool.

Mehrere Dialekte, eine API

Der gleiche Builder läuft gegen Postgres, MySQL und SQLite – ein Tausch des Treibers genügt, gelegentlich ist dialektspezifischer Code nötig.

Das Gesamtbild

Drei Kernideen hinter Knex

SQL als JavaScript zusammensetzen, Werte sicher binden und denselben Builder über mehrere Datenbank-Dialekte hinweg nutzen.

Der Builder

Komponieren

knex('users') startet eine Abfrage, und jede verkettete Methode fügt eine Klausel hinzu, bis du das Ergebnis mit await abrufst.

Der Dialekt

Übersetzen

Knex wandelt die Builder-Kette in parametrisiertes SQL für den konfigurierten Client um und setzt Identifier entsprechend dem Dialekt in Anführungszeichen.

Der Pool

Verbinden

Ein Connection Pool liegt jeder Abfrage zugrunde, und die Knex-Instanz verwaltet dessen Lebenszyklus.

Datenmodell

Die Users-Tabelle, einmal definiert

Definiert in einer Migration mit dem Schema Builder, sodass derselbe JavaScript-Code die Tabelle in jeder Umgebung erstellt.

Die Users-TabelleKnex migration
  • idincrementsAuto-inkrementierender Integer als Primary Key
  • emailstring(255)Nicht nullbar und eindeutig, erzwungen durch die Datenbank
  • display_namestring(120)Der öffentliche Name, der im Interface angezeigt wird
  • created_attimestampStandardmäßig das now() der Datenbank beim Einfügen

Definiert in einer Migration mit dem Schema Builder, sodass derselbe JavaScript-Code die Tabelle in jeder Umgebung erstellt.

Eine kurze Geschichte

Ein Builder, der sein ORM überlebt hat

  1. 2013

    Knex wird veröffentlicht

    Ein Query Builder für Node.js erscheint und bündelt Migrationen sowie einen Schema Builder mit der Query API.

    13
  2. 2016

    Objection.js baut auf Knex auf

    Eine ORM-Schicht erscheint über dem Builder und beweist, dass beide kombiniert werden können, statt sich zu ersetzen.

    16
  3. 2018

    Async/await überall

    Knex-Abfragen werden zu Thenables, sodass der Builder natürlich in modernes JavaScript passt.

    18
  4. 2020

    Knex 1.0 und TypeScript

    Ein stabiler Major-Release erscheint mit verbesserten Typisierungen und kontinuierlicher Dialekt-Unterstützung.

    20
  5. 2023

    Knex 3.x

    Der Builder behält eine kleine, stabile Oberfläche bei, während er eine Abhängigkeit für größere ORMs bleibt.

    23

Der vollständige Leitfaden

Knex.js: Alles was Sie wissen müssen

Was ist Knex.js?

Knex.js ist ein SQL query builder für Node.js. Es ist bewusst kein ORM. Es bildet Zeilen nicht auf Klassen ab, verfolgt keinen Objektzustand und lädt keine Relationen eigenständig. Stattdessen ermöglicht es Ihnen, SQL-Statements als kombinierbare JavaScript-Objekte aufzubauen, jeden Wert sicher zu binden und diese gegen Postgres, MySQL, SQLite, MSSQL oder Oracle auszuführen.

Genau diese Zurückhaltung ist der Grund für seinen langfristigen Erfolg. Knex erschien 2013 und wurde zum Fundament, auf dem Objection.js und mehrere andere Bibliotheken aufbauen. Wenn Sie die Ergonomie eines ORM wünschen, können Sie eines darauf aufsetzen; wenn Sie nah am SQL bleiben wollen, bietet Knex bereits die richtige Abstraktionsebene.

Das mentale Modell ist simpel: Beginnen Sie mit einem Tabellennamen, verketten Sie Methoden, um das Statement zu beschreiben, und await es schließlich, um es auszuführen. Jede Kette wird letztendlich zu einer parametrisierten Query.

Warum überhaupt einen Query Builder verwenden?

SQL als Template-Strings zu schreiben scheint völlig in Ordnung, bis eine Abfrage ihre Struktur ändern muss. Ein Such-Endpunkt mit optionalen Filtern, eine Sortierung, die von der Benutzereingabe abhängt, und eine Pagination-Klausel sind mühsam per Konkatenation zusammenzusetzen – und gefährlich, wenn Werte direkt interpoliert werden.

Ein Builder löst beide Probleme. Bedingungen werden sauber angehängt, Werte werden zu gebundenen Parametern und Bezeichner werden für den jeweiligen Dialekt korrekt in Anführungszeichen gesetzt.

const query = db("users").select("id", "email");

if (search) {
  query.where("email", "ilike", `%${search}%`);
}
if (verifiedOnly) {
  query.whereNotNull("email_verified_at");
}

const users = await query.orderBy("created_at", "desc").limit(20);

Beachten Sie, dass search als Argument an where übergeben wird und nicht in einen String eingefügt wird. Knex sendet diesen Wert als Platzhalter an den Treiber, sodass er niemals die Struktur des Statements verändern kann.

Ein weiterer Vorteil ist die Portabilität. Dieselbe Builder-Kette läuft auf Postgres und MySQL mit nur einer Konfigurationsänderung, was besonders für die lokale Entwicklung und Tests mit SQLite nützlich ist.

Installation und die knexfile

Installiere Knex und den Treiber für deine Datenbank. Der Treiber ist ein separates Paket, da Knex keine Datenbank-Clients bündelt.

pnpm add knex pg
pnpm add -D @types/pg

Die Konfiguration befindet sich in einer knexfile, mit einem Eintrag pro Umgebung. Die CLI liest diese automatisch aus, und deine Anwendung importiert dasselbe Objekt.

import type { Knex } from "knex";

const config: { [key: string]: Knex.Config } = {
  development: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    migrations: { directory: "./migrations" },
    seeds: { directory: "./seeds" },
    pool: { min: 2, max: 10 },
  },
  production: {
    client: "pg",
    connection: process.env.DATABASE_URL,
    pool: { min: 2, max: 10 },
  },
};

export default config;

Erstelle eine einzige gemeinsame Instanz und importiere sie überall, anstatt knex() in jedem Modul aufzurufen. Eine Instanz besitzt einen Connection Pool; zusätzliche Instanzen vervielfachen die Verbindungen zur Datenbank.

import knex from "knex";
import config from "../knexfile";

export const db = knex(config.development);

Nutze TypeScript’s satisfies oder ein typisiertes Config-Objekt, damit der Editor falsch geschriebene Optionen erkennt, und speichere Secrets in der Umgebung anstatt in der Datei.

Migrationen mit dem Schema Builder

Migrationen steuern, wie sich das Schema im Laufe der Zeit verändert. Jede Datei enthält eine up, die die Änderung anwendet, und eine down, die diese rückgängig macht.

npx knex migrate:make create_users
npx knex migrate:latest
npx knex migrate:rollback
npx knex migrate:list

Die generierte Datei ist gewöhnliches TypeScript. Der Schema Builder beschreibt Tabellen mithilfe eines Callbacks, der ein table-Objekt erhält.

import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.createTable("users", (table) => {
    table.increments("id").primary();
    table.string("email", 255).notNullable().unique();
    table.string("display_name", 120).notNullable();
    table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists("users");
}

Knex protokolliert angewendete Migrationen in einer knex_migrations-Tabelle, sodass jede Umgebung genau weiß, welche Dateien bereits ausgeführt wurden. Schreiben Sie immer eine echte down; auch wenn Sie selten Rollbacks durchführen, dokumentiert dies, wie die Änderung rückgängig gemacht werden kann, und ermöglicht das Testen von Migrationen.

alterTable ändert eine bestehende Tabelle. Einige Änderungen, wie das Hinzufügen einer notNullable-Spalte zu einer Tabelle mit vorhandenen Zeilen, erfordern einen Standardwert oder einen Backfill-Schritt. Teilen Sie diese in zwei Migrationen auf: eine zum Hinzufügen der Spalte und zum Backfill, und eine weitere zum Hinzufügen des Constraints.

Seeds für lokale Daten

Seeds befüllen eine Datenbank mit bekannten Datensätzen für die Entwicklung und für Tests. Sie sind von Migrationen getrennt, da sie Daten und nicht die Struktur repräsentieren.

import type { Knex } from "knex";

export async function seed(knex: Knex): Promise<void> {
  await knex("users").del();
  await knex("users").insert([
    { email: "[email protected]", display_name: "Ada Lovelace" },
    { email: "[email protected]", display_name: "Linus Torvalds" },
  ]);
}

Führen Sie diese mit npx knex seed:run aus. Seeds sollten nach Möglichkeit idempotent sein. Das bedeutet in der Regel, dass die Tabelle geleert oder ein Upsert vor dem Einfügen verwendet wird, damit ein mehrfacher Durchlauf nicht zu Fehlern oder Duplikaten führt.

Queries erstellen

Eine Query beginnt mit einem Tabellennamen. Von dort aus werden Methoden verkettet, bis das Statement mit await aufgerufen wird.

const rows = await db("users")
  .select("id", "email", "display_name")
  .where("created_at", ">", since)
  .whereIn("role", ["admin", "editor"])
  .orderBy("created_at", "desc")
  .limit(50)
  .offset(0);

where akzeptiert verschiedene Formen: eine Spalte und einen Wert, eine Spalte, einen Operator und einen Wert oder ein Objekt mit Gleichheitsprüfungen. whereIn, whereNot, whereNull, whereBetween und whereExists decken die restlichen gängigen Prädikate ab. Gruppierte Bedingungen verwenden einen Callback, damit die Klammern an der richtigen Stelle gesetzt werden.

db("posts")
  .where("published", true)
  .andWhere((qb) => {
    qb.where("title", "ilike", `%${term}%`).orWhere("body", "ilike", `%${term}%`);
  });

Verwenden Sie .first(), wenn Sie eine einzelne Zeile erwarten, und pluck, wenn Sie ein flaches Array einer einzelnen Spalte benötigen.

const user = await db("users").where({ email }).first();
const emails = await db("users").pluck("email");

Joins, Aggregates und Group By

Joins werden genauso gelesen wie in SQL: zuerst eine Tabelle, dann das Spaltenpaar, das sie verknüpft.

const rows = await db("users as u")
  .join("posts as p", "p.author_id", "u.id")
  .whereNot("p.status", "draft")
  .groupBy("u.id", "u.email")
  .select("u.email")
  .count("p.id as post_count")
  .max("p.created_at as last_post_at")
  .orderBy("post_count", "desc")
  .limit(10);

join ist ein Inner Join, leftJoin behält nicht zugeordnete Zeilen aus der ersten Tabelle bei, und rightJoin sowie fullOuterJoin sind in Dialekten verfügbar, die diese unterstützen. Die Join-Bedingung kann auch ein Callback sein, wenn mehr als eine Klausel benötigt wird.

Aggregates nutzen .count(), .sum(), .avg(), .min() und .max(). Zwei Details führen oft zu Fehlern. Erstens muss jede nicht aggregierte Spalte im Select in groupBy erscheinen. Zweitens werden in Postgres die Werte für count und sum als Strings zurückgegeben, um einen Integer-Overflow zu vermeiden; casten oder parsen Sie diese daher, wenn Sie Zahlen benötigen.

const { count } = await db("users").count("* as count").first();
const total = Number(count);

Die returning-Klausel

Beim Einfügen einer Zeile möchte man in der Regel den generierten Primary Key oder die serverseitigen Standardwerte erhalten. In Postgres und MSSQL fordert .returning() die Datenbank auf, die Zeile zurückzusenden.

const [user] = await db("users")
  .insert({ email, display_name: displayName })
  .returning(["id", "email", "created_at"]);

Auch Updates und Deletes können Zeilen zurückgeben:

const updated = await db("posts")
  .where({ id })
  .update({ title, updated_at: db.fn.now() })
  .returning("*");

MySQL und ältere SQLite-Versionen unterstützen RETURNING nicht. Dort löst sich der Insert-Aufruf zur neuen ID auf, und man muss einen anschließenden Select ausführen. Es ist wichtig zu wissen, welchen Dialekt man anspricht, da dies einer der Punkte ist, an denen das Versprechen der Portabilität an seine Grenzen stößt.

Upserts und Conflict Handling

Ein gängiges Muster ist: „Füge diese Zeile ein, aber aktualisiere sie, falls sie bereits existiert“. Knex drückt dies mit onConflict aus, was bei Postgres und SQLite zu ON CONFLICT und bei MySQL zu ON DUPLICATE KEY UPDATE gemappt wird.

await db("users")
  .insert({ email, display_name: displayName, last_seen_at: db.fn.now() })
  .onConflict("email")
  .merge({
    display_name: displayName,
    last_seen_at: db.fn.now(),
  });

onConflict erwartet die eindeutige Spalte oder die Spalten, die den Konflikt definieren. merge aktualisiert die aufgelisteten Spalten mit den Werten der neuen Zeile, während ignore den Insert komplett überspringt. Dies ist der sichere Weg, um einen Sync- oder Webhook-Handler idempotent zu gestalten, da die Datenbank die Race Condition zwischen zwei gleichzeitigen Schreibvorgängen auflöst und nicht Ihr Anwendungscode.

Transaktionen

Eine Transaktion gruppiert Statements, sodass diese gemeinsam committet oder zurückgerollt (rollback) werden. db.transaction übergibt ein Transaktionsobjekt an den Callback, und jedes Statement innerhalb dieses Blocks muss dieses Objekt verwenden.

await db.transaction(async (trx) => {
  const account = await trx("accounts").where({ id: fromId }).first();

  if (!account || account.balance_cents < 5000) {
    throw new Error("insufficient_funds");
  }

  await trx("accounts").where({ id: fromId }).decrement("balance_cents", 5000);
  await trx("accounts").where({ id: toId }).increment("balance_cents", 5000);
});

Das Werfen eines Fehlers (Throwing) führt zum Rollback der Transaktion und lehnt das Promise ab. Ein Return-Statement committet die Transaktion. Der häufigste Fehler ist die Vermischung von trx und db innerhalb desselben Blocks: Abfragen, die über db ausgeführt werden, nutzen eine andere Connection aus dem Pool und laufen außerhalb der Transaktion, weshalb sie nicht zurückgerollt werden.

Für die manuelle Steuerung gibt db.transaction() ohne Callback ein Transaktionsobjekt mit den Methoden commit und rollback zurück. Bevorzugen Sie die Callback-Form; dabei ist es schwieriger, eine Connection zu leaken, weil man vergessen hat, den Commit auszuführen.

Raw Queries, wenn sie benötigt werden

Der Builder kann nicht alles ausdrücken, und das versucht er auch gar nicht. knex.raw führt einen SQL-String mit gebundenen Werten aus.

const result = await db.raw(
  `select date_trunc('day', created_at) as day, count(*) as signups
   from users
   where created_at >= ?
   group by 1
   order by 1`,
  [since],
);

const rows = result.rows;

Verwenden Sie immer ?-Platzhalter und übergeben Sie die Werte im Array. Wenn Sie diese direkt in den String interpolieren, führen Sie genau das Injection-Risiko wieder ein, das der Builder eigentlich verhindern soll. Sie können mit whereRaw oder select(db.raw(...)) ein Raw-Fragment in eine Builder-Kette einbetten, wenn nur ein Teil der Query handgeschriebenes SQL benötigt.

Window-Funktionen, rekursive CTEs, COPY und dialektspezifische Operatoren sind gute Gründe, auf Raw-Queries auszuweichen. Eine Ranking-Query beispielsweise ist klarer geschrieben als mühsam aus Builder-Fragmenten zusammengesetzt:

const { rows } = await db.raw(
  `select email, score,
          row_number() over (order by score desc) as rank
   from leaderboard
   where season = ?`,
  [season],
);

Halten Sie solche Fragmente klein und kommentieren Sie diese; verwenden Sie für alles andere drumherum bevorzugt den Builder.

Connection Pooling

Jede Abfrage läuft über einen Pool von Verbindungen, der von tarn.js verwaltet wird. Standardmäßig sind ein Minimum von zwei und ein Maximum von zehn Verbindungen eingestellt, was für einen einzelnen Prozess angemessen ist, in der Produktion jedoch optimiert werden muss.

const db = knex({
  client: "pg",
  connection: process.env.DATABASE_URL,
  pool: { min: 2, max: 10, acquireTimeoutMillis: 30_000 },
});

Die Größe des Pools sollte basierend auf dem max_connections der Datenbank festgelegt und auf alle Applikations-Instanzen verteilt werden. Ein zu großer Pool ist ebenso schädlich wie ein zu kleiner: Zu viele Verbindungen erschöpfen den Arbeitsspeicher und die Prozesstabelle des Servers. Setzen Sie einen Pooler wie PgBouncer davor, wenn Sie viele Instanzen oder serverless functions betreiben.

Rufen Sie await db.destroy() auf, wenn der Prozess heruntergefahren wird. Ohne diesen Aufruf halten offene Verbindungen den Event Loop aktiv, wodurch ein Graceful Shutdown hängen bleibt.

Knex mit Objection.js kombinieren

Knex gibt einfache Zeilen zurück. Wenn Sie hingegen Models, Relationen und Lifecycle-Hooks benötigen, ist Objection.js die richtige Ergänzung. Es baut direkt auf Knex auf und nutzt dieselbe Verbindung.

import { Model } from "objection";
import { db } from "./db";

Model.knex(db);

class User extends Model {
  static tableName = "users";

  static relationMappings = {
    posts: {
      relation: Model.HasManyRelation,
      modelClass: Post,
      join: { from: "users.id", to: "posts.author_id" },
    },
  };
}

const user = await User.query()
  .withGraphFetched("posts")
  .findOne({ email });

Diese Schichtung ist die pragmatische Lösung für Teams, die ein ORM wünschen, aber auf zu starke Abstraktionen verzichten möchten: Knex kümmert sich um SQL und Migrationen, Objection fügt das Objektmodell hinzu, und Sie können jederzeit zu Knex zurückkehren, wenn eine Abfrage nicht in das Model-Layer passt.

Du schreibst immer noch SQL

Das Wichtigste, das man über Knex verstehen muss, ist, dass es SQL nicht versteckt. Es ordnet es neu, parametrisiert es und setzt Anführungszeichen, aber das resultierende Statement ist genau das, was du selbst geschrieben hättest.

Das hat zwei Konsequenzen. Die erste ist positiv: Wenn du eine Knex-Chain liest, erkennst du sofort die Query. Debugging bedeutet hier, .toSQL() auszugeben und den Plan zu lesen, anstatt zu raten, welches SQL generiert wurde.

Die zweite Konsequenz ist eine Verantwortung: Ein Builder bewahrt dich nicht vor einem fehlenden Index, einem SELECT * über einer breiten Tabelle oder einem versehentlichen Cross Join. Du entwirfst weiterhin das Schema, fügst die Indexe hinzu und prüfst EXPLAIN ANALYZE. Knex nimmt dir das mühsame Zusammenbauen von Strings ab, aber nicht die Notwendigkeit, die Datenbank zu verstehen.

Best Practices

  • Erstellen Sie eine einzige Knex-Instanz und importieren Sie diese; rufen Sie knex() niemals pro Modul auf.
  • Binden Sie jeden Wert als Parameter und verwenden Sie ?-Platzhalter in Raw-Queries.
  • Versionieren Sie jede Schema-Änderung als Migration mit einer echten down-Methode.
  • Teilen Sie riskante Änderungen in separate Migrationen auf: Zuerst hinzufügen und Backfill durchführen, Constraints erst später setzen.
  • Verwenden Sie .returning(), sofern der Dialekt dies unterstützt, andernfalls weichen Sie auf ein nachfolgendes Select aus.
  • Verwenden Sie innerhalb von db.transaction immer das Transaction-Objekt und niemals die Top-Level-Instanz.
  • Dimensionieren Sie den Connection Pool basierend auf dem Datenbank-Limit und rufen Sie db.destroy() beim Shutdown auf.
  • Casten Sie Postgres count- und sum-Ergebnisse in Zahlen, bevor Sie arithmetische Operationen durchführen.
  • Geben Sie .toSQL() aus, wenn eine Query Sie überrascht, und bestätigen Sie die Performance mit EXPLAIN ANALYZE.

Häufige Fehler

  • Vermischung von trx und db in einer einzigen Transaction, wodurch Teile der Arbeit nicht durch den Rollback rückgängig gemacht werden.
  • Interpolation von Werten in knex.raw-Strings anstatt Platzhalter zu verwenden.
  • Vergessen von groupBy für nicht-aggregierte Spalten, was zu einem SQL-Fehler führt.
  • Behandeln des von count zurückgegebenen Strings als Zahl, was zu "10" + 1 führt.
  • Die Annahme, dass .returning() auf MySQL und SQLite identisch funktioniert.
  • Ein Pool wird unbegrenzt oder größer konfiguriert, als die Datenbank verarbeiten kann.
  • Erstellen einer neuen Knex-Instanz pro Request, wodurch die Verbindungen erschöpft werden.
  • Manuelles Bearbeiten eines Production-Schemas, was zu Divergenzen zwischen den Umgebungen führt.
  • Ignorieren der down-Methode, bis ein Rollback tatsächlich benötigt wird.

Wie geht es weiter?

Knex ist ein idealer Anlaufpunkt, wenn Sie gerne nah an SQL arbeiten, und bietet eine solide Grundlage, falls Sie später ein ORM darauf aufsetzen möchten. Lesen Sie den SQL-Guide, um die Statements, die der Builder generiert, zu perfektionieren, und den PostgreSQL-Guide für Informationen zu Indizes, Transaktionen und Query-Plänen. Wenn Sie stattdessen einen typisierten Schema-First-Client bevorzugen, generiert Prisma einen solchen für Sie, während Drizzle ORM näher am Builder-Stil bleibt und eine vollständige TypeScript-Inferenz bietet.

In der Praxis

Migrationen, Abfragen, Joins, Transaktionen

Die vier Dinge, die du mit Knex am häufigsten tust – vom Erstellen der Tabelle bis hin zu atomaren Änderungen.

migrations/20240101_create_users.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.createTable("users", (table) => {
    table.increments("id").primary();
    table.string("email", 255).notNullable().unique();
    table.string("display_name", 120).notNullable();
    table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.dropTableIfExists("users");
}

Komponierbarer Builder vs. String-Konkatenation

Das manuelle Erstellen von SQL mit Template-Strings lädt zu Injection- und Quoting-Fehlern ein. Der Builder bindet Werte und escaped Identifier für dich.

Bevorzugt
const query = db("users").select("id", "email");

if (search) {
  query.where("email", "ilike", `%${search}%`);
}

const rows = await query.orderBy("created_at", "desc");
Vermeiden
let sql = "select id, email from users";

if (search) {
  // The value is interpolated straight into the statement.
  sql += ` where email ilike '%${search}%'`;
}

const rows = await db.raw(sql);

Migrationen vs. manuelles Ändern des Schemas

Eine Migration ist eine versionierte, umkehrbare Datei, die in jeder Umgebung in der gleichen Reihenfolge ausgeführt wird. Manuell bearbeitete Schemas driften auseinander und können nicht reproduziert werden.

Bevorzugt
export async function up(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.boolean("email_verified").notNullable().defaultTo(false);
  });
}

export async function down(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.dropColumn("email_verified");
  });
}
Vermeiden
-- Run once in a terminal against production,
-- then forgotten and never applied to staging.
ALTER TABLE users ADD COLUMN email_verified boolean;

Abwägungen

Ist ein Query Builder die richtige Ebene für dich?

Knex liegt zwischen rohem SQL und einem vollständigen ORM. Diese Position ist für einige Teams ein Vorteil und für andere ein unnötiger Mehraufwand.

Strengths

  • SQL bleibt sichtbar

    Die Kette liest sich wie das Statement, das sie erzeugt. Es gibt keine versteckte Query-Generierung und keine Magie, die debuggt werden muss.

  • Sicher durch Konstruktion

    Werte werden immer als Parameter gebunden, Identifier werden pro Dialekt in Anführungszeichen gesetzt und dynamische Filter lassen sich ohne Injection-Risiko zusammensetzen.

  • Migrationen und Seeds inklusive

    Schema Builder, Migrationen und Seeds sind im selben Paket enthalten, sodass du kein zweites Tool zur Datenbankverwaltung benötigst.

Trade-offs

  • Keine Models oder Relationen

    Knex gibt einfache Zeilen zurück. Du schreibst dein eigenes Mapping, und wenn du möchtest, dass Relationen automatisch geladen werden, benötigst du Objection.js oder ein ORM darüber.

  • Typisierung ist manuell

    Zeilen sind standardmäßig lose typisiert. Du annotierst die Ergebnisformen selbst oder fügst eine typisierte Schicht hinzu, was mit wachsendem Schema Disziplin erfordert.

  • Dialekt-Unterschiede schlagen durch

    Die API ist portabel, SQL jedoch nicht. JSON-Operatoren, Upserts und returning verhalten sich unterschiedlich; teste daher auf der Datenbank, die du deployst.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Knex.js zu lernen?

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