Documentation Drizzle

Drizzle ORMのNeon接続概要

Drizzle ORMは、NeonのサーバーレスPostgreSQLデータベースへの接続において、HTTPおよびWebSocketベースの専用ドライバーを提供しています。Neonは、サーバーレス環境に最適化されたデータベースサービスで、HTTPやWebSocket経由での接続をサポートしており、特にエッジ環境での利用に適しています。本記事では、Drizzle ORMを使用してNeonに接続するための設定方法と、HTTPとWebSocket接続の違いについて詳しく解説します。

HTTP接続を使用したNeonとの連携

HTTPベースのneon-httpドライバーは、非対話的なトランザクションやリクエスト単位の簡単なクエリ操作に適しています。サーバーレス環境での高速なデータベースアクセスが求められる場合に最適です。

ステップ1:パッケージのインストール

まず、Drizzle ORMとNeon用のHTTP接続ドライバーをインストールします。また、drizzle-kitも追加します。

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

ステップ2:HTTPドライバーの初期化とクエリの実行

NeonのHTTP接続を設定するには、DATABASE_URLを使用してデータベースの接続URLを環境変数から読み込みます。

import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';

// Neon HTTP クライアントを作成
const sql = neon(process.env.DATABASE_URL!);

// Drizzle ORMを初期化
export const db = drizzle(sql);

// シンプルなクエリの実行
const result = await db.execute('SELECT 1');

HTTP接続はリクエストごとに接続が開かれるため、単一トランザクションや簡単な読み取り操作に向いています。特にCloudflare WorkersやVercel Edge Functionsなどのエッジランタイムで最適なパフォーマンスを発揮します。

ステップ3:スキーマの定義

PostgreSQL用のスキーマを定義します。

// schema.ts
import { pgTable, serial, varchar, text, timestamp, boolean } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

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').notNull().default(true),
  created_at: timestamp('created_at', { withTimezone: true })
    .notNull()
    .default(sql`now()`),
  updated_at: timestamp('updated_at', { withTimezone: true })
    .notNull()
    .default(sql`now()`)
    .$onUpdate(() => new Date())
});

// TypeScript型推論
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

ステップ4:CRUD操作の実装

import { db } from './database';
import { users, type NewUser } from './schema';
import { eq, like, desc } from 'drizzle-orm';

// ユーザーを作成
export async function createUser(newUser: NewUser) {
  try {
    const result = await db.insert(users).values(newUser).returning();
    return result[0];
  } catch (error) {
    console.error('Failed to create user:', error);
    throw error;
  }
}

// すべてのユーザーを取得
export async function getAllUsers() {
  try {
    return await db.select().from(users).orderBy(desc(users.created_at));
  } catch (error) {
    console.error('Failed to get users:', error);
    throw error;
  }
}

// IDでユーザーを取得
export async function getUserById(id: number) {
  try {
    const result = await db.select().from(users).where(eq(users.id, id));
    return result[0];
  } catch (error) {
    console.error('Failed to get user:', error);
    throw error;
  }
}

// メールでユーザーを検索
export async function searchUsersByEmail(query: string) {
  try {
    return await db
      .select()
      .from(users)
      .where(like(users.email, `%${query}%`));
  } catch (error) {
    console.error('Failed to search users:', error);
    throw error;
  }
}

// ユーザーを更新
export async function updateUser(id: number, updates: Partial<NewUser>) {
  try {
    const result = await db
      .update(users)
      .set(updates)
      .where(eq(users.id, id))
      .returning();
    return result[0];
  } catch (error) {
    console.error('Failed to update user:', error);
    throw error;
  }
}

// ユーザーを削除
export async function deleteUser(id: number) {
  try {
    await db.delete(users).where(eq(users.id, id));
  } catch (error) {
    console.error('Failed to delete user:', error);
    throw error;
  }
}

エッジランタイムでの使用例(Cloudflare Workers)

// worker.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import { users } from './schema';
import { eq } from 'drizzle-orm';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Neon HTTP接続を作成
    const sql = neon(env.DATABASE_URL);
    const db = drizzle(sql);

    try {
      const url = new URL(request.url);
      const path = url.pathname;

      // GET /users - すべてのユーザーを取得
      if (path === '/users' && request.method === 'GET') {
        const allUsers = await db.select().from(users);
        return Response.json(allUsers);
      }

      // GET /users/:id - 特定のユーザーを取得
      if (path.match(/^\/users\/\d+$/) && request.method === 'GET') {
        const id = parseInt(path.split('/')[2]);
        const user = await db.select().from(users).where(eq(users.id, id));

        if (user.length === 0) {
          return Response.json({ error: 'User not found' }, { status: 404 });
        }

        return Response.json(user[0]);
      }

      // POST /users - 新しいユーザーを作成
      if (path === '/users' && request.method === 'POST') {
        const body = await request.json();
        const newUser = await db.insert(users).values(body).returning();
        return Response.json(newUser[0], { status: 201 });
      }

      return Response.json({ error: 'Not found' }, { status: 404 });
    } catch (error) {
      console.error('Request failed:', error);
      return Response.json(
        { error: 'Internal server error' },
        { status: 500 }
      );
    }
  }
};

WebSocket接続を使用したNeonとの連携

WebSocketベースのneon-serverlessドライバーは、セッションの維持やインタラクティブなトランザクションが必要な場合に適しています。サーバーレス環境での対話型クエリや複数ステップのトランザクション処理に活用できます。

ステップ1:WebSocket環境のパッケージインストール

Node.jsでWebSocketを使用するには、wsbufferutilの追加が必要です。

npm install drizzle-orm @neondatabase/serverless ws bufferutil
npm install -D drizzle-kit

ステップ2:WebSocketドライバーの初期化とクエリの実行

WebSocket接続を初期化するには、Poolを使用してNeonの接続プールを作成します。

import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from 'ws';

// Node.js環境でWebSocketを設定
neonConfig.webSocketConstructor = ws;

// 接続プールを作成
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Drizzle ORMを初期化
export const db = drizzle(pool);

// クエリの実行
const result = await db.execute('SELECT 1');

WebSocketドライバーは、クライアントからの継続的な接続が必要な場合や、リッチなインタラクティブセッションが求められる環境に最適です。

ステップ3:トランザクションの実装

WebSocket接続では、複数のクエリをトランザクション内で実行できます。

import { db } from './database';
import { users } from './schema';

// トランザクション例
export async function transferUserData(fromId: number, toId: number) {
  try {
    await db.transaction(async (tx) => {
      // 1. 元のユーザーを取得
      const fromUser = await tx
        .select()
        .from(users)
        .where(eq(users.id, fromId));

      if (fromUser.length === 0) {
        throw new Error('Source user not found');
      }

      // 2. 対象ユーザーを更新
      await tx
        .update(users)
        .set({ bio: fromUser[0].bio })
        .where(eq(users.id, toId));

      // 3. 元のユーザーを無効化
      await tx
        .update(users)
        .set({ is_active: false })
        .where(eq(users.id, fromId));

      console.log('Transaction completed successfully');
    });
  } catch (error) {
    console.error('Transaction failed:', error);
    throw error;
  }
}

トランザクションは、すべての操作が成功するか、すべて失敗するかのいずれかを保証します。

ステップ4:接続プーリングとパフォーマンス最適化

import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';

// 接続プールの設定
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // 最大接続数
  idleTimeoutMillis: 30000, // アイドルタイムアウト(30秒)
  connectionTimeoutMillis: 10000 // 接続タイムアウト(10秒)
});

export const db = drizzle(pool);

// プリペアドステートメントを使用したクエリ
export async function getUsersByStatus(isActive: boolean) {
  return await db
    .select()
    .from(users)
    .where(eq(users.is_active, isActive))
    .prepare('get_users_by_status')
    .execute();
}

// バッチ処理
export async function createMultipleUsers(newUsers: NewUser[]) {
  try {
    const result = await db.insert(users).values(newUsers).returning();
    return result;
  } catch (error) {
    console.error('Batch insert failed:', error);
    throw error;
  }
}

WebSocketを使用する際の注意点

Node.jsでは、グローバルにWebSocketが定義されていないため、wsbufferutilのインストールが必要です。また、NeonのWebSocket接続がリクエスト間でセッションを維持するため、リアルタイムアプリケーションや高度なデータ処理に向いています。

環境変数の設定

Neonダッシュボードから接続文字列を取得し、環境変数に設定します。

# .env
DATABASE_URL="postgresql://user:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"

エラーハンドリングのベストプラクティス

import { db } from './database';
import { users } from './schema';
import { eq } from 'drizzle-orm';

export async function safeCreateUser(email: string, name: string) {
  try {
    const result = await db
      .insert(users)
      .values({ email, name })
      .returning();

    return { success: true, user: result[0] };
  } catch (error: any) {
    // PostgreSQLエラーコードをチェック
    if (error.code === '23505') {
      // Unique constraint violation
      return { success: false, error: 'Email already exists' };
    }

    if (error.code === '23503') {
      // Foreign key constraint violation
      return { success: false, error: 'Referenced record does not exist' };
    }

    // その他のエラー
    console.error('Database error:', error);
    return { success: false, error: 'Database operation failed' };
  }
}

// タイムアウト処理
export async function queryWithTimeout<T>(
  queryFn: () => Promise<T>,
  timeoutMs: number = 5000
): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error('Query timeout')), timeoutMs)
  );

  return Promise.race([queryFn(), timeoutPromise]);
}

// 使用例
const users = await queryWithTimeout(
  () => db.select().from(users),
  3000 // 3秒タイムアウト
);

HTTPとWebSocketの接続方式の比較

接続方式用途特徴推奨環境
HTTP (neon-http)単一の簡単なクエリ操作、非対話的な処理に最適リクエストごとに接続、レスポンスが迅速、コールドスタート時間が短いCloudflare Workers、Vercel Edge Functions
WebSocket (neon-serverless)継続的な接続や対話型セッションに適するセッション管理、対話的トランザクション対応、接続プーリングNode.js、Next.js API Routes、Express

実践例:Next.js App RouterでのNeon統合

// app/api/users/route.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { NextRequest, NextResponse } from 'next/server';

const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);

// GET /api/users
export async function GET() {
  try {
    const allUsers = await db.select().from(users);
    return NextResponse.json(allUsers);
  } catch (error) {
    console.error('Failed to fetch users:', error);
    return NextResponse.json(
      { error: 'Failed to fetch users' },
      { status: 500 }
    );
  }
}

// POST /api/users
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // 簡易的なバリデーション
    if (!body.email || !body.name) {
      return NextResponse.json(
        { error: 'Email and name are required' },
        { status: 400 }
      );
    }

    const newUser = await db
      .insert(users)
      .values({
        email: body.email,
        name: body.name,
        bio: body.bio
      })
      .returning();

    return NextResponse.json(newUser[0], { status: 201 });
  } catch (error: any) {
    if (error.code === '23505') {
      return NextResponse.json(
        { error: 'Email already exists' },
        { status: 409 }
      );
    }

    console.error('Failed to create user:', error);
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    );
  }
}

Vercel Edge Functionsでの使用例

// app/api/edge/route.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import { users } from '@/db/schema';

export const runtime = 'edge';

const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);

export async function GET() {
  try {
    // Edge環境で高速に実行
    const allUsers = await db.select().from(users).limit(100);

    return new Response(JSON.stringify(allUsers), {
      headers: {
        'content-type': 'application/json',
        'cache-control': 'public, s-maxage=60, stale-while-revalidate=120'
      }
    });
  } catch (error) {
    console.error('Edge function error:', error);
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500, headers: { 'content-type': 'application/json' } }
    );
  }
}

パフォーマンス最適化のヒント

1. HTTP接続を使用する場合

// キャッシュ戦略を実装
import { unstable_cache } from 'next/cache';

export const getCachedUsers = unstable_cache(
  async () => {
    return await db.select().from(users);
  },
  ['users-list'],
  { revalidate: 60 } // 60秒ごとに再検証
);

2. インデックスの活用

// drizzle.config.ts でマイグレーションを設定
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!
  }
});

// スキーマでインデックスを定義
import { pgTable, serial, varchar, index } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }).notNull()
}, (table) => ({
  emailIdx: index('email_idx').on(table.email)
}));

3. バッチクエリ

// 複数のクエリを並列実行
const [users, posts, comments] = await Promise.all([
  db.select().from(usersTable),
  db.select().from(postsTable),
  db.select().from(commentsTable)
]);

まとめ

Drizzle ORMは、NeonのHTTPおよびWebSocket接続をサポートすることで、サーバーレス環境での柔軟なデータベース操作を実現します。

主なポイント:

  1. HTTP接続 (neon-http): エッジランタイムに最適。Cloudflare Workers、Vercel Edge Functionsで高速動作
  2. WebSocket接続 (neon-serverless): Node.js環境でトランザクションと接続プーリングをサポート
  3. 型安全性: TypeScriptとの完全な統合により、コンパイル時のエラー検出
  4. エラーハンドリング: PostgreSQLエラーコードを活用した適切なエラー処理
  5. パフォーマンス最適化: プリペアドステートメント、バッチ処理、キャッシュ戦略

シンプルなリクエストにはHTTP接続、インタラクティブでリアルタイム性が求められるアプリケーションにはWebSocket接続を選択することで、Neonのポテンシャルを最大限に引き出すことが可能です。

参考文献

円