Initial commit: Portfolio Website
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>
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user