概要
この記事では、Drizzle ORMのHTTP Proxyドライバを使用し、エッジランタイムやサーバレス環境からHTTP経由でデータベースに接続する方法を詳しく解説します。HTTP Proxyドライバは、直接的なTCP接続ができない環境(Cloudflare Workers、Vercel Edge Functions、Next.js Edge Runtimeなど)で、HTTPエンドポイント経由でデータベースクエリを実行できる仕組みです。
Drizzle HTTP Proxyとは
HTTP Proxyドライバの概要
Drizzle ORMのHTTP Proxyドライバは、以下のような環境で有用です:
- エッジランタイム: Cloudflare Workers、Vercel Edge Functionsなど、TCPソケット接続が制限されている環境
- サーバレス環境: コールドスタート時のコネクション確立時間を削減したい場合
- セキュリティ: データベース接続をHTTPサーバー経由で一元管理し、クライアントから直接データベースに接続しない構成
HTTP Proxyの仕組み
┌─────────────┐ HTTP ┌──────────────┐ TCP ┌──────────┐
│ Edge │ ────────────> │ Proxy Server │ ─────────────> │ Database │
│ Function │ (Query) │ (Node.js) │ (Direct) │ │
└─────────────┘ └──────────────┘ └──────────┘
- クライアント(Edge Function)がHTTP経由でクエリをProxyサーバーに送信
- Proxyサーバーがデータベースに直接接続してクエリを実行
- 結果をHTTPレスポンスとしてクライアントに返す
Drizzle ORMとは
Drizzle ORMは、JavaScriptやTypeScriptプロジェクト向けの軽量で型安全なORMです。HTTP Proxyドライバを使用することで、エッジ環境でも完全なDrizzle ORMの機能を利用できます。
HTTP Proxyを用いたデータベース接続手順
必要なパッケージのインストール
Proxyサーバーとクライアント用のパッケージをインストールします。
# Proxyサーバー用(Node.js環境)
npm install drizzle-orm pg express
# クライアント用(Edge環境)
npm install drizzle-orm
ステップ1: Proxyサーバーの構築
まず、データベースに直接接続できるNode.js環境でProxyサーバーを構築します。
// server/proxy-server.ts
import express from 'express';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
const app = express();
app.use(express.json());
// PostgreSQL接続プールの作成
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
const db = drizzle(pool);
// Proxyエンドポイント
app.post('/query', async (req, res) => {
try {
const { sql, params, method } = req.body;
// 認証チェック(本番環境では必須)
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${process.env.PROXY_SECRET}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
// SQLクエリを実行
let result;
if (method === 'all') {
result = await pool.query(sql, params);
res.json({ rows: result.rows });
} else if (method === 'execute') {
result = await pool.query(sql, params);
res.json({ rows: result.rows, rowCount: result.rowCount });
} else {
res.status(400).json({ error: 'Invalid method' });
}
} catch (error: any) {
console.error('Query error:', error);
res.status(500).json({ error: error.message });
}
});
// ヘルスチェックエンドポイント
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Drizzle Proxy Server running on port ${PORT}`);
});
環境変数の設定(.env):
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
PROXY_SECRET=your-secret-key-here
PORT=3000
ステップ2: クライアント側の設定
Proxyサーバーに接続するクライアント(Edge Function)を実装します。
// client/db.ts(Edge Function内)
import { drizzle } from 'drizzle-orm/http-proxy';
// HTTP Proxy クライアントの作成
export const db = drizzle(async (sql, params, method) => {
const response = await fetch('https://your-proxy-server.com/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PROXY_SECRET}`
},
body: JSON.stringify({ sql, params, method })
});
if (!response.ok) {
throw new Error(`Proxy request failed: ${response.statusText}`);
}
const result = await response.json();
return { rows: result.rows };
});
ステップ3: スキーマ定義
共通のスキーマファイルを作成します(サーバーとクライアントで共有)。
// shared/schema.ts
import { pgTable, serial, varchar, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
bio: text('bio'),
is_active: boolean('is_active').default(true).notNull(),
created_at: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
updated_at: timestamp('updated_at', { mode: 'date' }).defaultNow().$onUpdate(() => new Date())
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
user_id: serial('user_id').notNull().references(() => users.id),
title: varchar('title', { length: 200 }).notNull(),
content: text('content'),
published: boolean('published').default(false),
created_at: timestamp('created_at', { mode: 'date' }).defaultNow().notNull()
});
// 型定義
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
基本的なデータ操作 - HTTP Proxy経由のCRUD
Edge Function内でHTTP Proxy経由でデータ操作を行う例を紹介します。
データの挿入
import { db } from './db';
import { users, type NewUser } from './schema';
// 単一ユーザーの挿入
async function createUser(data: NewUser) {
const result = await db
.insert(users)
.values(data)
.returning();
return result[0];
}
// 使用例
const newUser = await createUser({
name: '山田太郎',
email: 'yamada@example.com',
bio: 'フルスタックエンジニア'
});
// 複数ユーザーの一括挿入
async function createMultipleUsers(userList: NewUser[]) {
return await db
.insert(users)
.values(userList)
.returning();
}
データの取得
import { eq, like, and, desc } from 'drizzle-orm';
// 全ユーザー取得
async function getAllUsers() {
return await db.select().from(users);
}
// IDで取得
async function getUserById(id: number) {
const result = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1);
return result[0];
}
// メールアドレスで検索
async function getUserByEmail(email: string) {
const result = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
return result[0];
}
// 名前で部分一致検索
async function searchUsers(nameQuery: string) {
return await db
.select()
.from(users)
.where(like(users.name, `%${nameQuery}%`));
}
// アクティブユーザーを取得(ソート付き)
async function getActiveUsers() {
return await db
.select()
.from(users)
.where(eq(users.is_active, true))
.orderBy(desc(users.created_at));
}
// JOIN を使った取得(ユーザーと投稿)
import { posts } from './schema';
async function getUsersWithPosts() {
return await db
.select({
user: users,
post: posts
})
.from(users)
.leftJoin(posts, eq(users.id, posts.user_id));
}
データの更新
// ユーザー情報の更新
async function updateUser(id: number, data: Partial<NewUser>) {
const result = await db
.update(users)
.set({
...data,
updated_at: new Date()
})
.where(eq(users.id, id))
.returning();
return result[0];
}
// 使用例
await updateUser(1, {
name: '山田花子',
bio: '更新された自己紹介'
});
// メールアドレスのみ更新
async function updateUserEmail(id: number, newEmail: string) {
return await db
.update(users)
.set({ email: newEmail, updated_at: new Date() })
.where(eq(users.id, id))
.returning();
}
// 複数条件での一括更新
async function deactivateInactiveUsers(days: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
return await db
.update(users)
.set({ is_active: false })
.where(
and(
eq(users.is_active, true),
sql`${users.updated_at} < ${cutoffDate}`
)
);
}
データの削除
// ユーザー削除
async function deleteUser(id: number) {
const result = await db
.delete(users)
.where(eq(users.id, id))
.returning();
return result[0];
}
// 条件付き削除
async function deleteInactiveUsers() {
return await db
.delete(users)
.where(eq(users.is_active, false))
.returning();
}
// カスケード削除(関連投稿も削除)
async function deleteUserWithPosts(userId: number) {
// トランザクションで実行
await db.transaction(async (tx) => {
// まず投稿を削除
await tx.delete(posts).where(eq(posts.user_id, userId));
// 次にユーザーを削除
await tx.delete(users).where(eq(users.id, userId));
});
}
各プラットフォームでの実装例
Cloudflare Workers
// cloudflare-worker.ts
import { db } from './db';
import { users } from './schema';
import { eq } from 'drizzle-orm';
export interface Env {
PROXY_URL: string;
PROXY_SECRET: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// GET /users
if (url.pathname === '/users' && request.method === 'GET') {
const allUsers = await db.select().from(users);
return new Response(JSON.stringify(allUsers), {
headers: { 'Content-Type': 'application/json' }
});
}
// GET /users/:id
const userMatch = url.pathname.match(/^\/users\/(\d+)$/);
if (userMatch && request.method === 'GET') {
const id = parseInt(userMatch[1]);
const user = await db
.select()
.from(users)
.where(eq(users.id, id))
.limit(1);
if (user.length === 0) {
return new Response('Not found', { status: 404 });
}
return new Response(JSON.stringify(user[0]), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not found', { status: 404 });
}
};
Vercel Edge Functions
// pages/api/users.ts
import { db } from '../../lib/db';
import { users } from '../../lib/schema';
import type { NextRequest } from 'next/server';
export const config = {
runtime: 'edge'
};
export default async function handler(req: NextRequest) {
try {
const allUsers = await db.select().from(users);
return new Response(JSON.stringify({
success: true,
data: allUsers
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error: any) {
return new Response(JSON.stringify({
success: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
セキュリティのベストプラクティス
1. 認証トークンの管理
// server/proxy-server.ts
import crypto from 'crypto';
// トークンのハッシュ化
const VALID_TOKENS = new Set([
crypto.createHash('sha256').update(process.env.PROXY_SECRET!).digest('hex')
]);
app.post('/query', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing authorization' });
}
const token = authHeader.slice(7);
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
if (!VALID_TOKENS.has(hashedToken)) {
return res.status(401).json({ error: 'Invalid token' });
}
// クエリ実行...
});
2. レートリミッティング
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 100リクエストまで
message: 'Too many requests from this IP'
});
app.post('/query', limiter, async (req, res) => {
// クエリ実行...
});
3. クエリのサニタイズとバリデーション
app.post('/query', async (req, res) => {
const { sql, params, method } = req.body;
// 危険なSQL操作を防ぐ
const dangerousPatterns = [
/DROP\s+TABLE/i,
/DELETE\s+FROM.*WHERE\s+1\s*=\s*1/i,
/TRUNCATE/i
];
if (dangerousPatterns.some(pattern => pattern.test(sql))) {
return res.status(403).json({ error: 'Forbidden operation' });
}
// クエリ実行...
});
パフォーマンス最適化
コネクションプーリング
// Proxyサーバー側でコネクションプールを設定
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // 最大接続数
idleTimeoutMillis: 30000, // アイドルタイムアウト
connectionTimeoutMillis: 2000 // 接続タイムアウト
});
リクエストのキャッシング
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 60 }); // 60秒キャッシュ
app.post('/query', async (req, res) => {
const { sql, params } = req.body;
const cacheKey = `${sql}:${JSON.stringify(params)}`;
// キャッシュチェック
const cached = cache.get(cacheKey);
if (cached) {
return res.json(cached);
}
// クエリ実行
const result = await pool.query(sql, params);
const response = { rows: result.rows };
// キャッシュに保存
cache.set(cacheKey, response);
res.json(response);
});
エラー対処と注意点
1. Proxy接続エラー
問題: fetch failed または Connection refused エラー
対処法:
// リトライ機能付きのfetchラッパー
async function fetchWithRetry(url: string, options: RequestInit, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
// 使用例
export const db = drizzle(async (sql, params, method) => {
const response = await fetchWithRetry('https://your-proxy.com/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.PROXY_SECRET}`
},
body: JSON.stringify({ sql, params, method })
});
return await response.json();
});
2. タイムアウトエラー
問題: クエリ実行時間が長すぎてタイムアウトする
対処法:
// Proxyサーバー側でタイムアウト設定
app.post('/query', async (req, res) => {
const timeoutMs = 10000; // 10秒
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Query timeout')), timeoutMs);
});
try {
const result = await Promise.race([
pool.query(sql, params),
timeoutPromise
]);
res.json({ rows: result.rows });
} catch (error: any) {
if (error.message === 'Query timeout') {
res.status(408).json({ error: 'Request timeout' });
} else {
res.status(500).json({ error: error.message });
}
}
});
3. CORS エラー
問題: ブラウザからProxyにアクセスするとCORSエラーが発生
対処法:
import cors from 'cors';
// 開発環境
app.use(cors());
// 本番環境(特定のオリジンのみ許可)
app.use(cors({
origin: ['https://your-app.com', 'https://your-app.vercel.app'],
credentials: true
}));
4. メモリリークとコネクション管理
問題: 長時間稼働後にメモリ使用量が増加する
対処法:
// コネクションプールのイベントハンドリング
pool.on('error', (err, client) => {
console.error('Unexpected error on idle client', err);
});
pool.on('connect', (client) => {
console.log('New client connected');
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing pool...');
await pool.end();
process.exit(0);
});
5. HTTP Proxyの制限事項
注意点:
- レイテンシ: TCP直接接続より遅い(HTTP オーバーヘッド)
- トランザクション: 複雑なトランザクションは通常の接続を推奨
- ストリーミング: 大量データのストリーミングには不向き
- コスト: Proxyサーバーの稼働コストが追加で発生
推奨事項:
- 読み取り専用クエリに使用
- 短いクエリに最適
- 書き込みはバッチ処理を検討
- 本当にHTTP Proxyが必要か検討(直接接続が可能なら直接接続を使用)
6. デプロイのベストプラクティス
// 環境変数の検証
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is not set');
}
if (!process.env.PROXY_SECRET) {
throw new Error('PROXY_SECRET is not set');
}
// ヘルスチェックの実装
app.get('/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({
status: 'healthy',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message
});
}
});
// メトリクスの収集
let queryCount = 0;
let errorCount = 0;
app.post('/query', async (req, res) => {
queryCount++;
try {
// クエリ実行...
} catch (error) {
errorCount++;
throw error;
}
});
app.get('/metrics', (req, res) => {
res.json({
queryCount,
errorCount,
successRate: ((queryCount - errorCount) / queryCount * 100).toFixed(2)
});
});
まとめ
Drizzle ORMのHTTP Proxyドライバを使用することで、エッジランタイムやサーバレス環境からHTTP経由でデータベースにアクセスできるようになります。TCP接続が制限されている環境でも、完全なDrizzle ORMの機能を活用できる点が大きな利点です。
主な特徴:
- エッジ対応: Cloudflare Workers、Vercel Edge Functionsなどで利用可能
- 型安全性: 通常のDrizzle ORMと同じ型安全なクエリが使用可能
- セキュリティ: Proxyサーバーで一元的にアクセス制御と認証を実装
- 柔軟性: 既存のデータベースをそのまま使用可能
- パフォーマンス: コネクションプーリングとキャッシングで最適化
使用を検討すべきケース:
- エッジランタイムでデータベースアクセスが必要な場合
- TCP接続が制限されている環境
- データベース接続を一元管理したい場合
代替案を検討すべきケース:
- TCP直接接続が可能な環境(より低レイテンシ)
- 複雑なトランザクションが必要な場合
- 大量データのストリーミングが必要な場合
HTTP Proxyドライバは、エッジコンピューティング時代のデータベースアクセスパターンとして、適切な場面で使用することで大きな価値を発揮します。