PlanetScale

データベースMySQLサーバーレスブランチングスキーマ管理分散データベース開発者ツール

データベースプラットフォーム

PlanetScale

概要

PlanetScaleは、MySQL互換のサーバーレスデータベースプラットフォームです。革新的なブランチング機能により、データベーススキーマをGitのように管理でき、開発者がソフトウェア開発と同じワークフローでデータベース変更を安全に実行できます。YouTubeのバックエンドを支えるVitessテクノロジーを基盤とし、グローバル分散とスケーラビリティを提供します。

詳細

データベースブランチング

PlanetScaleの最大の特徴は、データベースのブランチング機能です。開発者はGitブランチを作成するように、本番データベースから新しいブランチを作成し、スキーマ変更をテストできます。この機能により、安全なスキーマ展開とチーム間でのコラボレーションが可能になります。

MySQL互換性

完全なMySQL互換性を提供し、既存のMySQLアプリケーションをそのまま移行できます。ORM、ドライバー、ツールチェーンがそのまま使用でき、学習コストを最小限に抑えられます。

サーバーレスアーキテクチャ

自動スケーリング機能により、トラフィックに応じてデータベースが動的にスケールします。使用した分だけの従量課金制で、アイドル時間中はコストが発生しません。

高度なセキュリティ

業界標準の暗号化、ネットワークセキュリティ、アクセス制御を提供します。SOC 2 Type IIコンプライアンスに準拠し、エンタープライズグレードのセキュリティ要件を満たします。

メリット・デメリット

メリット

  • 革新的なブランチング機能: データベーススキーマをGitのように管理でき、安全な変更とロールバックが可能
  • 完全なMySQL互換性: 既存のMySQLアプリケーションとツールをそのまま使用可能
  • 自動スケーリング: トラフィックに応じた動的なスケーリングで高可用性を実現
  • スキーマ分析: 潜在的な問題を事前に検出するスキーマ分析ツール
  • ゼロダウンタイム展開: 本番環境への影響なしにスキーマ変更を適用
  • グローバル分散: 世界中のユーザーに低レイテンシでデータを提供

デメリット

  • 外部キー制約の制限: Vitessの制約により一部の外部キー機能が制限される
  • ストアドプロシージャ非対応: MySQL特有の一部機能が使用できない場合がある
  • 新しいプラットフォーム: 比較的新しいサービスのため、長期的な実績が限定的
  • ベンダーロックイン: PlanetScale特有の機能への依存が移行を困難にする可能性
  • 高トラフィック時のコスト: 大規模なワークロードでは従来のホストソリューションより高価になる場合がある

参考ページ

実装の例

セットアップ

# 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`)
  }
}