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,433 @@
|
||||
# PlanetScale: Serverless MySQL mit Vitess
|
||||
|
||||
**Meta-Description:** PlanetScale für skalierbares MySQL. Database Branching, Non-Blocking Schema Changes und Vitess-powered Performance.
|
||||
|
||||
**Keywords:** PlanetScale, MySQL, Vitess, Serverless Database, Database Branching, Schema Migrations, Horizontal Scaling
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
PlanetScale bringt **YouTube-Scale** zu MySQL. Powered by Vitess (entwickelt für YouTube) bietet es Horizontal Sharding, Non-Blocking Schema Changes und Database Branching – ohne die Komplexität selbst zu managen.
|
||||
|
||||
---
|
||||
|
||||
## PlanetScale Features
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PLANETSCALE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Vitess Foundation: │
|
||||
│ ├── Horizontal Sharding (Auto) │
|
||||
│ ├── Connection Pooling │
|
||||
│ ├── Query Routing │
|
||||
│ └── Petabyte-Scale Proven │
|
||||
│ │
|
||||
│ Developer Experience: │
|
||||
│ ├── Database Branching (wie Git) │
|
||||
│ ├── Non-Blocking Schema Changes │
|
||||
│ ├── Schema Revert (1-Click Rollback) │
|
||||
│ └── PlanetScale Insights (Query Analytics) │
|
||||
│ │
|
||||
│ Serverless Benefits: │
|
||||
│ ├── No Cold Starts │
|
||||
│ ├── Auto-Scaling │
|
||||
│ ├── Per-Query Pricing │
|
||||
│ └── Edge-Compatible Driver │
|
||||
│ │
|
||||
│ Security: │
|
||||
│ ├── No Foreign Keys (by Design - Performance) │
|
||||
│ ├── Branch Permissions │
|
||||
│ └── Audit Logs │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# PlanetScale CLI installieren
|
||||
brew install planetscale/tap/pscale
|
||||
|
||||
# Login
|
||||
pscale auth login
|
||||
|
||||
# Database erstellen
|
||||
pscale database create my-app --region eu-west
|
||||
|
||||
# Branch erstellen
|
||||
pscale branch create my-app main
|
||||
|
||||
# Local Proxy (Development)
|
||||
pscale connect my-app main --port 3309
|
||||
```
|
||||
|
||||
```bash
|
||||
# Dependencies
|
||||
npm install @planetscale/database
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Connection
|
||||
|
||||
```typescript
|
||||
// lib/planetscale.ts
|
||||
import { connect } from '@planetscale/database';
|
||||
|
||||
const config = {
|
||||
host: process.env.DATABASE_HOST,
|
||||
username: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD
|
||||
};
|
||||
|
||||
export const conn = connect(config);
|
||||
|
||||
// Für Edge (Fetch-basiert)
|
||||
export async function query<T>(
|
||||
sql: string,
|
||||
args?: unknown[]
|
||||
): Promise<T[]> {
|
||||
const results = await conn.execute(sql, args);
|
||||
return results.rows as T[];
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Mit Drizzle ORM
|
||||
import { drizzle } from 'drizzle-orm/planetscale-serverless';
|
||||
import { connect } from '@planetscale/database';
|
||||
import * as schema from './schema';
|
||||
|
||||
const connection = connect({
|
||||
host: process.env.DATABASE_HOST,
|
||||
username: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD
|
||||
});
|
||||
|
||||
export const db = drizzle(connection, { schema });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Definition (Drizzle)
|
||||
|
||||
```typescript
|
||||
// src/db/schema.ts
|
||||
import {
|
||||
mysqlTable,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
timestamp,
|
||||
int,
|
||||
mysqlEnum
|
||||
} from 'drizzle-orm/mysql-core';
|
||||
|
||||
export const users = mysqlTable('users', {
|
||||
id: varchar('id', { length: 36 }).primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }),
|
||||
role: mysqlEnum('role', ['user', 'admin']).default('user'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
export const posts = mysqlTable('posts', {
|
||||
id: varchar('id', { length: 36 }).primaryKey(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
content: text('content'),
|
||||
slug: varchar('slug', { length: 255 }).notNull().unique(),
|
||||
published: boolean('published').default(false),
|
||||
authorId: varchar('author_id', { length: 36 }).notNull(),
|
||||
// Kein FOREIGN KEY - PlanetScale Design Decision
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
|
||||
});
|
||||
|
||||
// Index für Performance
|
||||
export const postsAuthorIdx = mysqlTable('posts', {
|
||||
authorId: varchar('author_id', { length: 36 })
|
||||
}).indexes((t) => ({
|
||||
authorIdx: index('author_idx').on(t.authorId)
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CRUD Operations
|
||||
|
||||
```typescript
|
||||
import { db } from '@/db';
|
||||
import { users, posts } from '@/db/schema';
|
||||
import { eq, and, desc, like, sql } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// CREATE
|
||||
async function createUser(email: string, name: string) {
|
||||
const id = nanoid();
|
||||
|
||||
await db.insert(users).values({
|
||||
id,
|
||||
email,
|
||||
name
|
||||
});
|
||||
|
||||
return { id, email, name };
|
||||
}
|
||||
|
||||
// READ
|
||||
async function getUserById(id: string) {
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, id));
|
||||
return user;
|
||||
}
|
||||
|
||||
// Manueller JOIN (ohne Foreign Keys)
|
||||
async function getPostsWithAuthors() {
|
||||
return await db
|
||||
.select({
|
||||
post: posts,
|
||||
author: {
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email
|
||||
}
|
||||
})
|
||||
.from(posts)
|
||||
.innerJoin(users, eq(posts.authorId, users.id))
|
||||
.where(eq(posts.published, true))
|
||||
.orderBy(desc(posts.createdAt));
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
async function updatePost(id: string, data: Partial<typeof posts.$inferInsert>) {
|
||||
await db
|
||||
.update(posts)
|
||||
.set(data)
|
||||
.where(eq(posts.id, id));
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async function deletePost(id: string) {
|
||||
await db.delete(posts).where(eq(posts.id, id));
|
||||
}
|
||||
|
||||
// Pagination
|
||||
async function getPostsPaginated(page: number, pageSize: number = 20) {
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const [results, countResult] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(eq(posts.published, true))
|
||||
.orderBy(desc(posts.createdAt))
|
||||
.limit(pageSize)
|
||||
.offset(offset),
|
||||
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(posts)
|
||||
.where(eq(posts.published, true))
|
||||
]);
|
||||
|
||||
return {
|
||||
posts: results,
|
||||
total: countResult[0].count,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(countResult[0].count / pageSize)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Branching Workflow
|
||||
|
||||
```bash
|
||||
# 1. Feature Branch erstellen
|
||||
pscale branch create my-app add-comments
|
||||
|
||||
# 2. Lokal verbinden
|
||||
pscale connect my-app add-comments --port 3310
|
||||
|
||||
# 3. Schema ändern (auf Branch)
|
||||
# In Code oder SQL:
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Schema-Migration auf Branch
|
||||
await db.execute(sql`
|
||||
CREATE TABLE comments (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
post_id VARCHAR(36) NOT NULL,
|
||||
author_id VARCHAR(36) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX post_idx (post_id),
|
||||
INDEX author_idx (author_id)
|
||||
)
|
||||
`);
|
||||
```
|
||||
|
||||
```bash
|
||||
# 4. Deploy Request erstellen
|
||||
pscale deploy-request create my-app add-comments
|
||||
|
||||
# 5. Review in Dashboard
|
||||
# Schema Diff wird angezeigt
|
||||
|
||||
# 6. Deploy (Non-Blocking!)
|
||||
pscale deploy-request deploy my-app 1
|
||||
|
||||
# 7. Branch löschen
|
||||
pscale branch delete my-app add-comments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Non-Blocking Schema Changes
|
||||
|
||||
```sql
|
||||
-- Diese Änderungen blockieren NICHT die Tabelle:
|
||||
|
||||
-- Column hinzufügen
|
||||
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);
|
||||
|
||||
-- Index hinzufügen (online)
|
||||
ALTER TABLE posts ADD INDEX slug_idx (slug);
|
||||
|
||||
-- Column umbenennen (via Ghost Tables)
|
||||
-- PlanetScale kopiert Daten im Hintergrund
|
||||
|
||||
-- Achtung: Foreign Keys nicht unterstützt!
|
||||
-- Stattdessen: Application-Level Referential Integrity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PlanetScale Insights
|
||||
|
||||
```typescript
|
||||
// Query Performance analysieren
|
||||
// In PlanetScale Dashboard: Insights Tab
|
||||
|
||||
// Slow Queries finden
|
||||
// - Queries > 100ms
|
||||
// - Table Scans
|
||||
// - Missing Indexes
|
||||
|
||||
// Beispiel: Index für häufige Query
|
||||
await db.execute(sql`
|
||||
CREATE INDEX posts_published_created_idx
|
||||
ON posts (published, created_at DESC)
|
||||
`);
|
||||
|
||||
// Composite Index für Filter + Sort
|
||||
await db.execute(sql`
|
||||
CREATE INDEX posts_author_published_idx
|
||||
ON posts (author_id, published, created_at DESC)
|
||||
`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge-Compatible Driver
|
||||
|
||||
```typescript
|
||||
// Cloudflare Workers / Vercel Edge
|
||||
import { connect } from '@planetscale/database';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET() {
|
||||
const conn = connect({
|
||||
host: process.env.DATABASE_HOST,
|
||||
username: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD
|
||||
});
|
||||
|
||||
const results = await conn.execute(
|
||||
'SELECT * FROM posts WHERE published = ? ORDER BY created_at DESC LIMIT 10',
|
||||
[true]
|
||||
);
|
||||
|
||||
return Response.json(results.rows);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referential Integrity ohne Foreign Keys
|
||||
|
||||
```typescript
|
||||
// Application-Level Constraints
|
||||
|
||||
// Bei Insert: Prüfen ob Author existiert
|
||||
async function createPost(data: NewPost) {
|
||||
const [author] = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(eq(users.id, data.authorId));
|
||||
|
||||
if (!author) {
|
||||
throw new Error('Author not found');
|
||||
}
|
||||
|
||||
return await db.insert(posts).values(data);
|
||||
}
|
||||
|
||||
// Bei Delete: Cascade manuell
|
||||
async function deleteUser(userId: string) {
|
||||
// Erst abhängige Daten löschen
|
||||
await db.delete(posts).where(eq(posts.authorId, userId));
|
||||
await db.delete(comments).where(eq(comments.authorId, userId));
|
||||
|
||||
// Dann User
|
||||
await db.delete(users).where(eq(users.id, userId));
|
||||
}
|
||||
|
||||
// Oder: Soft Delete Pattern
|
||||
const users = mysqlTable('users', {
|
||||
// ...
|
||||
deletedAt: timestamp('deleted_at')
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
PlanetScale bietet:
|
||||
|
||||
1. **Vitess-Powered**: YouTube-Scale für MySQL
|
||||
2. **Database Branching**: Git-Workflow für Datenbanken
|
||||
3. **Zero-Downtime Migrations**: Non-Blocking Schema Changes
|
||||
4. **Edge-Ready**: Fetch-basierter Driver für Serverless
|
||||
|
||||
Enterprise MySQL-Performance mit Developer-Friendly Workflow.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Database branches merging like Git, version control for databases"
|
||||
2. "MySQL scaling horizontally across servers, Vitess sharding concept"
|
||||
3. "Schema migration without downtime, zero-interruption deployment"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [PlanetScale Documentation](https://planetscale.com/docs)
|
||||
- [PlanetScale Branching](https://planetscale.com/docs/vitess/schema-changes/branching)
|
||||
- [Vitess Overview](https://vitess.io/)
|
||||
- [PlanetScale Database Driver](https://github.com/planetscale/database-js)
|
||||
Reference in New Issue
Block a user