Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
495 lines
13 KiB
Markdown
495 lines
13 KiB
Markdown
# Drizzle ORM: Die leichtgewichtige Prisma-Alternative
|
||
|
||
**Meta-Description:** Drizzle ORM für TypeScript. SQL-nah, serverless-optimiert und nur 7KB. Vergleich mit Prisma und Migration Guide.
|
||
|
||
**Keywords:** Drizzle ORM, TypeScript ORM, SQL Builder, Serverless Database, Prisma Alternative, Edge Computing, Lightweight ORM
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
Drizzle ORM ist ein **TypeScript-first ORM** mit SQL-naher Syntax. Mit nur ~7KB (gzipped) und null Dependencies ist es perfekt für Serverless und Edge Computing – wo Prismas Bundle-Size zum Problem wird.
|
||
|
||
---
|
||
|
||
## Drizzle vs Prisma
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ DRIZZLE vs PRISMA │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ DRIZZLE PRISMA │
|
||
│ ──────────────────── ──────────────────── │
|
||
│ ~7KB gzipped ~600KB+ (mit Engine) │
|
||
│ SQL-nah (Query Builder) Abstrahierte API │
|
||
│ Code-First Schema Schema-First (.prisma) │
|
||
│ Zero Dependencies Rust Binary Engine │
|
||
│ Instant Cold Starts Cold Start Issues │
|
||
│ │
|
||
│ Best for: Best for: │
|
||
│ • Serverless/Edge • Rapid Development │
|
||
│ • SQL-Erfahrene Devs • Schema-First Teams │
|
||
│ • Performance-Critical • Große Teams/Abstraction │
|
||
│ • Kleine Bundles • Prisma Studio │
|
||
│ │
|
||
│ Supported DBs: Supported DBs: │
|
||
│ PostgreSQL, MySQL, SQLite PostgreSQL, MySQL, │
|
||
│ Turso, Neon, PlanetScale SQLite, MongoDB, │
|
||
│ Cloudflare D1, Vercel SQL Server, CockroachDB │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Installation & Setup
|
||
|
||
```bash
|
||
# Installation
|
||
npm install drizzle-orm
|
||
npm install -D drizzle-kit
|
||
|
||
# Für PostgreSQL
|
||
npm install postgres
|
||
|
||
# Für MySQL
|
||
npm install mysql2
|
||
|
||
# Für SQLite/Turso
|
||
npm install @libsql/client
|
||
```
|
||
|
||
```typescript
|
||
// drizzle.config.ts
|
||
import { defineConfig } from 'drizzle-kit';
|
||
|
||
export default defineConfig({
|
||
schema: './src/db/schema.ts',
|
||
out: './drizzle',
|
||
dialect: 'postgresql',
|
||
dbCredentials: {
|
||
url: process.env.DATABASE_URL!
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## Schema Definition (Code-First)
|
||
|
||
```typescript
|
||
// src/db/schema.ts
|
||
import {
|
||
pgTable,
|
||
uuid,
|
||
text,
|
||
timestamp,
|
||
boolean,
|
||
integer,
|
||
pgEnum
|
||
} from 'drizzle-orm/pg-core';
|
||
import { relations } from 'drizzle-orm';
|
||
|
||
// Enum
|
||
export const roleEnum = pgEnum('role', ['user', 'admin', 'moderator']);
|
||
|
||
// Users Table
|
||
export const users = pgTable('users', {
|
||
id: uuid('id').defaultRandom().primaryKey(),
|
||
email: text('email').notNull().unique(),
|
||
name: text('name'),
|
||
role: roleEnum('role').default('user').notNull(),
|
||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
||
});
|
||
|
||
// Posts Table
|
||
export const posts = pgTable('posts', {
|
||
id: uuid('id').defaultRandom().primaryKey(),
|
||
title: text('title').notNull(),
|
||
content: text('content'),
|
||
published: boolean('published').default(false).notNull(),
|
||
authorId: uuid('author_id')
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: 'cascade' }),
|
||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
||
});
|
||
|
||
// Comments Table
|
||
export const comments = pgTable('comments', {
|
||
id: uuid('id').defaultRandom().primaryKey(),
|
||
content: text('content').notNull(),
|
||
postId: uuid('post_id')
|
||
.notNull()
|
||
.references(() => posts.id, { onDelete: 'cascade' }),
|
||
authorId: uuid('author_id')
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: 'cascade' }),
|
||
createdAt: timestamp('created_at').defaultNow().notNull()
|
||
});
|
||
|
||
// Relations
|
||
export const usersRelations = relations(users, ({ many }) => ({
|
||
posts: many(posts),
|
||
comments: many(comments)
|
||
}));
|
||
|
||
export const postsRelations = relations(posts, ({ one, many }) => ({
|
||
author: one(users, {
|
||
fields: [posts.authorId],
|
||
references: [users.id]
|
||
}),
|
||
comments: many(comments)
|
||
}));
|
||
|
||
export const commentsRelations = relations(comments, ({ one }) => ({
|
||
post: one(posts, {
|
||
fields: [comments.postId],
|
||
references: [posts.id]
|
||
}),
|
||
author: one(users, {
|
||
fields: [comments.authorId],
|
||
references: [users.id]
|
||
})
|
||
}));
|
||
|
||
// Type Inference
|
||
export type User = typeof users.$inferSelect;
|
||
export type NewUser = typeof users.$inferInsert;
|
||
export type Post = typeof posts.$inferSelect;
|
||
export type NewPost = typeof posts.$inferInsert;
|
||
```
|
||
|
||
---
|
||
|
||
## Database Connection
|
||
|
||
```typescript
|
||
// src/db/index.ts
|
||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||
import postgres from 'postgres';
|
||
import * as schema from './schema';
|
||
|
||
const connectionString = process.env.DATABASE_URL!;
|
||
|
||
// Für Queries
|
||
const queryClient = postgres(connectionString);
|
||
export const db = drizzle(queryClient, { schema });
|
||
|
||
// Für Migrations (separater Client)
|
||
const migrationClient = postgres(connectionString, { max: 1 });
|
||
export const migrationDb = drizzle(migrationClient);
|
||
```
|
||
|
||
```typescript
|
||
// Für Serverless (Neon, Vercel)
|
||
import { drizzle } from 'drizzle-orm/neon-http';
|
||
import { neon } from '@neondatabase/serverless';
|
||
import * as schema from './schema';
|
||
|
||
const sql = neon(process.env.DATABASE_URL!);
|
||
export const db = drizzle(sql, { schema });
|
||
```
|
||
|
||
```typescript
|
||
// Für Edge (Cloudflare D1)
|
||
import { drizzle } from 'drizzle-orm/d1';
|
||
import * as schema from './schema';
|
||
|
||
export interface Env {
|
||
DB: D1Database;
|
||
}
|
||
|
||
export default {
|
||
async fetch(request: Request, env: Env) {
|
||
const db = drizzle(env.DB, { schema });
|
||
// ...
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## CRUD Operations
|
||
|
||
```typescript
|
||
import { db } from '@/db';
|
||
import { users, posts, comments } from '@/db/schema';
|
||
import { eq, and, or, desc, asc, like, sql } from 'drizzle-orm';
|
||
|
||
// CREATE
|
||
async function createUser(data: NewUser) {
|
||
const [user] = await db
|
||
.insert(users)
|
||
.values(data)
|
||
.returning();
|
||
return user;
|
||
}
|
||
|
||
async function createManyUsers(data: NewUser[]) {
|
||
return await db
|
||
.insert(users)
|
||
.values(data)
|
||
.returning();
|
||
}
|
||
|
||
// READ - Single
|
||
async function getUserById(id: string) {
|
||
const [user] = await db
|
||
.select()
|
||
.from(users)
|
||
.where(eq(users.id, id))
|
||
.limit(1);
|
||
return user;
|
||
}
|
||
|
||
// READ - Mit Relations (Query API)
|
||
async function getUserWithPosts(id: string) {
|
||
const result = await db.query.users.findFirst({
|
||
where: eq(users.id, id),
|
||
with: {
|
||
posts: {
|
||
where: eq(posts.published, true),
|
||
orderBy: [desc(posts.createdAt)],
|
||
limit: 10
|
||
}
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
// READ - Liste mit Filtering
|
||
async function getPublishedPosts(page: number = 1, pageSize: number = 10) {
|
||
const offset = (page - 1) * pageSize;
|
||
|
||
const results = await db
|
||
.select({
|
||
id: posts.id,
|
||
title: posts.title,
|
||
content: posts.content,
|
||
createdAt: posts.createdAt,
|
||
authorName: users.name,
|
||
authorEmail: users.email
|
||
})
|
||
.from(posts)
|
||
.innerJoin(users, eq(posts.authorId, users.id))
|
||
.where(eq(posts.published, true))
|
||
.orderBy(desc(posts.createdAt))
|
||
.limit(pageSize)
|
||
.offset(offset);
|
||
|
||
return results;
|
||
}
|
||
|
||
// UPDATE
|
||
async function updatePost(id: string, data: Partial<NewPost>) {
|
||
const [updated] = await db
|
||
.update(posts)
|
||
.set({
|
||
...data,
|
||
updatedAt: new Date()
|
||
})
|
||
.where(eq(posts.id, id))
|
||
.returning();
|
||
return updated;
|
||
}
|
||
|
||
// UPSERT
|
||
async function upsertUser(email: string, data: Partial<NewUser>) {
|
||
const [user] = await db
|
||
.insert(users)
|
||
.values({ email, ...data })
|
||
.onConflictDoUpdate({
|
||
target: users.email,
|
||
set: { ...data, updatedAt: new Date() }
|
||
})
|
||
.returning();
|
||
return user;
|
||
}
|
||
|
||
// DELETE
|
||
async function deletePost(id: string) {
|
||
const [deleted] = await db
|
||
.delete(posts)
|
||
.where(eq(posts.id, id))
|
||
.returning();
|
||
return deleted;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Advanced Queries
|
||
|
||
```typescript
|
||
import { sql, count, avg, sum } from 'drizzle-orm';
|
||
|
||
// Aggregations
|
||
async function getPostStats() {
|
||
const [stats] = await db
|
||
.select({
|
||
totalPosts: count(posts.id),
|
||
publishedPosts: count(sql`CASE WHEN ${posts.published} THEN 1 END`),
|
||
})
|
||
.from(posts);
|
||
return stats;
|
||
}
|
||
|
||
// Group By
|
||
async function getPostsByAuthor() {
|
||
return await db
|
||
.select({
|
||
authorId: posts.authorId,
|
||
authorName: users.name,
|
||
postCount: count(posts.id)
|
||
})
|
||
.from(posts)
|
||
.innerJoin(users, eq(posts.authorId, users.id))
|
||
.groupBy(posts.authorId, users.name)
|
||
.orderBy(desc(count(posts.id)));
|
||
}
|
||
|
||
// Subqueries
|
||
async function getUsersWithPostCount() {
|
||
const postCountSubquery = db
|
||
.select({
|
||
authorId: posts.authorId,
|
||
count: count(posts.id).as('post_count')
|
||
})
|
||
.from(posts)
|
||
.groupBy(posts.authorId)
|
||
.as('post_counts');
|
||
|
||
return await db
|
||
.select({
|
||
id: users.id,
|
||
name: users.name,
|
||
postCount: postCountSubquery.count
|
||
})
|
||
.from(users)
|
||
.leftJoin(postCountSubquery, eq(users.id, postCountSubquery.authorId));
|
||
}
|
||
|
||
// Raw SQL
|
||
async function searchPosts(query: string) {
|
||
return await db.execute(sql`
|
||
SELECT * FROM posts
|
||
WHERE to_tsvector('german', title || ' ' || content)
|
||
@@ plainto_tsquery('german', ${query})
|
||
`);
|
||
}
|
||
|
||
// Transactions
|
||
async function createPostWithComments(
|
||
post: NewPost,
|
||
comments: { content: string; authorId: string }[]
|
||
) {
|
||
return await db.transaction(async (tx) => {
|
||
const [newPost] = await tx
|
||
.insert(posts)
|
||
.values(post)
|
||
.returning();
|
||
|
||
if (comments.length > 0) {
|
||
await tx.insert(comments).values(
|
||
comments.map(c => ({
|
||
...c,
|
||
postId: newPost.id
|
||
}))
|
||
);
|
||
}
|
||
|
||
return newPost;
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Migrations
|
||
|
||
```bash
|
||
# Schema-Änderungen generieren
|
||
npx drizzle-kit generate
|
||
|
||
# Migrations ausführen
|
||
npx drizzle-kit migrate
|
||
|
||
# Push (Development - ohne Migration Files)
|
||
npx drizzle-kit push
|
||
|
||
# Studio (GUI)
|
||
npx drizzle-kit studio
|
||
```
|
||
|
||
```typescript
|
||
// Programmatische Migration
|
||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||
import { migrationDb } from './db';
|
||
|
||
async function runMigrations() {
|
||
await migrate(migrationDb, { migrationsFolder: './drizzle' });
|
||
console.log('Migrations complete');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Zod Integration
|
||
|
||
```typescript
|
||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
||
import { z } from 'zod';
|
||
import { users, posts } from './schema';
|
||
|
||
// Auto-generierte Schemas
|
||
const insertUserSchema = createInsertSchema(users);
|
||
const selectUserSchema = createSelectSchema(users);
|
||
|
||
// Mit Erweiterungen
|
||
const createUserSchema = createInsertSchema(users, {
|
||
email: z.string().email('Ungültige E-Mail'),
|
||
name: z.string().min(2, 'Name zu kurz').optional()
|
||
}).omit({ id: true, createdAt: true, updatedAt: true });
|
||
|
||
const updateUserSchema = createUserSchema.partial();
|
||
|
||
// Verwendung
|
||
async function createUserValidated(input: unknown) {
|
||
const data = createUserSchema.parse(input);
|
||
return await createUser(data);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Drizzle ORM bietet:
|
||
|
||
1. **Minimal Footprint**: ~7KB, zero dependencies
|
||
2. **SQL-Kontrolle**: Transparente, vorhersagbare Queries
|
||
3. **Type-Safety**: Volle TypeScript-Integration
|
||
4. **Edge-Ready**: Perfekt für Serverless/Edge
|
||
|
||
Für Performance-kritische und serverless Anwendungen ist Drizzle die bessere Wahl.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Lightweight feather next to database icons, minimal bundle size concept"
|
||
2. "SQL code transforming into TypeScript types, type-safe ORM visualization"
|
||
3. "Edge computing nodes with database connections, serverless architecture"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [Drizzle ORM Documentation](https://orm.drizzle.team/)
|
||
- [Drizzle vs Prisma Comparison](https://www.prisma.io/docs/orm/more/comparisons/prisma-and-drizzle)
|
||
- [Drizzle vs Prisma 2026](https://medium.com/@codabu/drizzle-vs-prisma-choosing-the-right-typescript-orm-in-2026-deep-dive-63abb6aa882b)
|
||
- [Drizzle Kit](https://orm.drizzle.team/kit-docs/overview)
|