- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
434 lines
11 KiB
Markdown
434 lines
11 KiB
Markdown
# 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)
|