PlanetScale
データベースプラットフォーム
PlanetScale
概要
PlanetScaleは、MySQL互換のサーバーレスデータベースプラットフォームです。革新的なブランチング機能により、データベーススキーマをGitのように管理でき、開発者がソフトウェア開発と同じワークフローでデータベース変更を安全に実行できます。YouTubeのバックエンドを支えるVitessテクノロジーを基盤とし、グローバル分散とスケーラビリティを提供します。
詳細
データベースブランチング
PlanetScaleの最大の特徴は、データベースのブランチング機能です。開発者はGitブランチを作成するように、本番データベースから新しいブランチを作成し、スキーマ変更をテストできます。この機能により、安全なスキーマ展開とチーム間でのコラボレーションが可能になります。
MySQL互換性
完全なMySQL互換性を提供し、既存のMySQLアプリケーションをそのまま移行できます。ORM、ドライバー、ツールチェーンがそのまま使用でき、学習コストを最小限に抑えられます。
サーバーレスアーキテクチャ
自動スケーリング機能により、トラフィックに応じてデータベースが動的にスケールします。使用した分だけの従量課金制で、アイドル時間中はコストが発生しません。
高度なセキュリティ
業界標準の暗号化、ネットワークセキュリティ、アクセス制御を提供します。SOC 2 Type IIコンプライアンスに準拠し、エンタープライズグレードのセキュリティ要件を満たします。
メリット・デメリット
メリット
- 革新的なブランチング機能: データベーススキーマをGitのように管理でき、安全な変更とロールバックが可能
- 完全なMySQL互換性: 既存のMySQLアプリケーションとツールをそのまま使用可能
- 自動スケーリング: トラフィックに応じた動的なスケーリングで高可用性を実現
- スキーマ分析: 潜在的な問題を事前に検出するスキーマ分析ツール
- ゼロダウンタイム展開: 本番環境への影響なしにスキーマ変更を適用
- グローバル分散: 世界中のユーザーに低レイテンシでデータを提供
デメリット
- 外部キー制約の制限: Vitessの制約により一部の外部キー機能が制限される
- ストアドプロシージャ非対応: MySQL特有の一部機能が使用できない場合がある
- 新しいプラットフォーム: 比較的新しいサービスのため、長期的な実績が限定的
- ベンダーロックイン: PlanetScale特有の機能への依存が移行を困難にする可能性
- 高トラフィック時のコスト: 大規模なワークロードでは従来のホストソリューションより高価になる場合がある
参考ページ
- 公式サイト: https://planetscale.com/
- ドキュメント: https://planetscale.com/docs/
- JavaScript SDK: https://github.com/planetscale/database-js
- サポート: https://planetscale.com/support/
- ブログ: https://planetscale.com/blog/
実装の例
セットアップ
# PlanetScale CLIをインストール
npm install -g @planetscale/cli
# PlanetScaleにログイン
pscale auth login
# 新しいデータベースを作成
pscale database create my-database --region us-east
# 開発ブランチを作成
pscale branch create my-database development
スキーマ設計
-- メインブランチでのスキーマ作成
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_published (published)
);
-- 開発ブランチでスキーマ変更をテスト
CREATE TABLE comments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_post_id (post_id),
INDEX idx_user_id (user_id)
);
データ操作
import { connect } from '@planetscale/database-js'
// データベース接続の設定
const config = {
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
}
const conn = connect(config)
// 基本的なクエリ操作
async function createUser(email, name) {
const result = await conn.execute(
'INSERT INTO users (email, name) VALUES (?, ?)',
[email, name]
)
return result.insertId
}
async function getUserById(id) {
const results = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[id]
)
return results.rows[0]
}
async function updateUser(id, updates) {
const setClause = Object.keys(updates)
.map(key => `${key} = ?`)
.join(', ')
const values = [...Object.values(updates), id]
await conn.execute(
`UPDATE users SET ${setClause} WHERE id = ?`,
values
)
}
// トランザクション処理
async function createPostWithUser(userEmail, userName, postTitle, postContent) {
const results = await conn.transaction(async (tx) => {
// ユーザーを作成
const userResult = await tx.execute(
'INSERT INTO users (email, name) VALUES (?, ?)',
[userEmail, userName]
)
// 投稿を作成
const postResult = await tx.execute(
'INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)',
[userResult.insertId, postTitle, postContent]
)
return {
userId: userResult.insertId,
postId: postResult.insertId
}
})
return results
}
スケーリング
# ブランチの管理
pscale branch list my-database
pscale branch create my-database feature/new-schema
pscale branch diff my-database feature/new-schema main
# deploy requestの作成(スキーマ変更の提案)
pscale deploy-request create my-database feature/new-schema
# deploy requestの確認とマージ
pscale deploy-request list my-database
pscale deploy-request deploy my-database 1
# 読み取りレプリカの作成
pscale branch create my-database read-replica --restore-from main --read-only
# データベースの監視
pscale database insights my-database
pscale branch insights my-database main
バックアップ・復旧
// 自動バックアップは PlanetScale で管理されるため、
// 特定時点への復旧やブランチング機能を活用
async function createBackupBranch() {
// CLI コマンドで特定時点のブランチを作成
// pscale branch create my-database backup-2024-01-01 --restore-from main@2024-01-01T00:00:00Z
// または JavaScript SDK でデータエクスポート
const exportData = await conn.execute(`
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, ORDINAL_POSITION
`)
return exportData.rows
}
// データベース統計の取得
async function getDatabaseStats() {
const stats = await conn.execute(`
SELECT
TABLE_NAME,
TABLE_ROWS,
DATA_LENGTH,
INDEX_LENGTH,
DATA_FREE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
`)
return stats.rows
}
統合
// Express.js との統合例
import express from 'express'
import { connect } from '@planetscale/database-js'
const app = express()
app.use(express.json())
const db = connect({
url: process.env.DATABASE_URL
})
// REST API エンドポイント
app.get('/api/users/:id', async (req, res) => {
try {
const result = await db.execute(
'SELECT id, email, name, created_at FROM users WHERE id = ?',
[req.params.id]
)
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' })
}
res.json(result.rows[0])
} catch (error) {
console.error('Database error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})
app.post('/api/users', async (req, res) => {
try {
const { email, name } = req.body
const result = await db.execute(
'INSERT INTO users (email, name) VALUES (?, ?)',
[email, name]
)
res.status(201).json({
id: result.insertId,
email,
name
})
} catch (error) {
if (error.message.includes('Duplicate entry')) {
return res.status(409).json({ error: 'Email already exists' })
}
console.error('Database error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})
// Next.js API Routes での使用例
export default async function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
try {
const posts = await db.execute(`
SELECT p.*, u.name as author_name
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.published = true
ORDER BY p.created_at DESC
LIMIT 10
`)
res.status(200).json(posts.rows)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch posts' })
}
break
default:
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}