日曜日, 8月 3, 2025
日曜日, 8月 3, 2025
- Advertisment -
ホームニューステックニュースOpenNext + Drizzle で Cloudflare D1 環境を最も楽に構築する

OpenNext + Drizzle で Cloudflare D1 環境を最も楽に構築する


OpenNext + Cloudflare D1 を使った開発で、ローカル環境での開発・テスト・本番への移行を効率的に行いたいと考えたことはありませんか。

愚直に実現しようとすると、環境ごとに異なる設定ファイルを用意したり、テストデータの管理に手間がかかります。Drizzle のエコシステム(drizzle-ormdrizzle-kitdrizzle-seed)を活用することで、これらの課題を解決し、シームレスな開発体験を実現する方法を紹介します。

この記事では、実際の @opennextjs/cloudflare プロジェクト構成を参考に、ローカル開発からテスト実装、本番デプロイまでの一連の流れを解説します。

package.json

{
  "dependencies": {
    "@libsql/client": "^0.15.10",
    "@opennextjs/cloudflare": "^1.6.2",
    "drizzle-orm": "^0.44.4"
  },
  "devDependencies": {
    "drizzle-kit": "^0.31.4",
    "drizzle-seed": "^0.3.1",
    "vitest": "^3.2.4",
    "wrangler": "^4.26.1"
  }
}

この記事で構築する最終的な関連したプロジェクト構造は以下のようになります。

./
├── drizzle/
│   ├── migrations/           
│   │   └── 0000_init.sql
│   └── schema.ts            
├── tests/
│   ├── db.test.ts           
│   └── db.ts                
├── drizzle.config.ts        
├── wrangler.jsonc           
└── package.json

1. 必要なパッケージのインストール

まず、必要なパッケージをインストールします。

npm install drizzle-orm @libsql/client
npm install -D drizzle-kit drizzle-seed vitest

2. Cloudflare D1 の設定

wrangler.jsonc に D1 データベースの情報を追記します。

には、D1 データベースの「データベース名」と「データベースID」を指定します。値の確認・発行方法は後述します。

wrangler.jsonc

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-03-01",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "",
      "database_id": "",
      "migrations_dir": "drizzle/migrations"
    }
  ]
}

値の確認・発行方法

  1. Cloudflare ダッシュボードから確認

    • Cloudflare Dashboard にログインし、
      対象アカウントの「ストレージとデータベース」→「D1 SQL データベース」へ移動する
    • 作成済みのデータベース一覧から、該当データベースを選択する
    • は一覧や詳細画面での表示名

    • は詳細画面の「UUID」

      • 例: e216461a-74c3-40b2-8819-9fa351827304

ダッシュボード上の  の情報

  1. Wrangler CLI で新規作成する場合

    • 下記コマンド成功時に出力される JSON フィールドの値を用いる
      • database_name
      • database_id
    npx wrangler d1 create my-app-dev
    
    Successfully created DB 'my-app-dev' in region APAC
    Created your new D1 database.
    
    {
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "my-app-dev",
          "database_id": "e216461a-74c3-40b2-8819-9fa351827304"
        }
      ]
    }
    

これらの値を wrangler.jsonc の該当箇所にコピー&ペーストしてください。

3. 環境別のデータベース設定

drizzle.config.ts本番環境とローカル環境の設定を記載します。

環境の切り分けには NODE_ENV 環境変数を利用しており、
NODE_ENV=production の場合は本番用、そうでない場合はローカル用の設定が適用されます。

drizzle.config.ts

import { readdirSync } from "fs"
import { defineConfig } from 'drizzle-kit'

const isProduction = process.env.NODE_ENV === 'production';


const sqliteDirPath = '.wrangler/state/v3/d1/miniflare-D1DatabaseObject';
const sqliteFilePath = readdirSync(sqliteDirPath).find(file => file.endsWith('.sqlite'));

const config = isProduction ? defineConfig({
  out: './drizzle/migrations',
  schema: './drizzle/schema.ts',
  dialect: 'sqlite',
  driver: 'd1-http',
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
    token: process.env.CLOUDFLARE_D1_TOKEN!,
  }
}) : defineConfig({
  out: './drizzle/migrations',
  schema: './drizzle/schema.ts',
  dialect: 'sqlite',
  dbCredentials: {
    url: `${sqliteDirPath}/${sqliteFilePath!}`,
  }
});

export default config;

Cloudflare D1 の本番環境の設定には、以下の環境変数が必要です。

  • CLOUDFLARE_ACCOUNT_ID
  • CLOUDFLARE_DATABASE_ID
  • CLOUDFLARE_D1_TOKEN

本番環境設定の取得方法に関しては下記を参考にしてください。

https://zenn.dev/arafipro/articles/2024-07-24-drizzle-d1-table#drizzle.config.ts-1

4. データベーススキーマの定義

drizzle/schema.ts で以下の2つのテーブルを定義します。

  • conversations テーブル
    チャットの会話単位を管理するテーブル

  • messages テーブル
    各会話に紐づくメッセージを管理するテーブル、
    conversationIdconversations テーブルとリレーションする。

drizzle/schema.ts

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';

export const conversations = sqliteTable('conversations', {
  id: text('id').primaryKey(),
  title: text('title'),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .default(sql`(unixepoch())`),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
    .notNull()
    .default(sql`(unixepoch())`),
});

export const messages = sqliteTable('messages', {
  id: text('id').primaryKey(),
  conversationId: text('conversation_id')
    .notNull()
    .references(() => conversations.id, { onDelete: 'cascade' }),
  role: text('role', { enum: ['user', 'system'] }).notNull(),
  content: text('content').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .default(sql`(unixepoch())`),
});

5. テスト環境の構築

tests/db.ts でテスト用のユーティリティ関数を作成します。テスト用データベースの作成・マイグレーションの適用・テストデータの投入などを行う関数をまとめています。

tests/db.ts

import { createClient } from '@libsql/client/node';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from '../drizzle/schema';
import { readFileSync } from 'fs';
import { join } from 'path';
import { seed, reset } from 'drizzle-seed';

export async function createTestDatabase() {
  const client = createClient({ url: ':memory:' });
  const db = drizzle(client, { schema });
  
  
  const migrationsDir = join(process.cwd(), 'drizzle/migrations');
  const migrationFiles = (
    await Promise.resolve(
      require('fs').readdirSync(migrationsDir)
        .filter((file: string) => file.endsWith('.sql'))
        .sort()
    )
  );

  
  for (const file of migrationFiles) {
    const migrationSQL = readFileSync(join(migrationsDir, file), 'utf-8');
    const statements = migrationSQL
      .split('--> statement-breakpoint')
      .map(stmt => stmt.trim())
      .filter(stmt => stmt.length > 0);

    for (const statement of statements) {
      await client.execute(statement);
    }
  }
  
  return { client, db };
}

export async function seedTestDataWithDrizzleSeed(
  db: any, 
  options?: { count?: number; seed?: number }
) {
  const { count = 3, seed: seedValue = 12345 } = options || {};
  
  
  await seed(db as any, schema, { count, seed: seedValue }).refine((f) => ({
    conversations: {
      count: 3,
      columns: {
        title: f.valuesFromArray({
          values: [
            'AI System Chat',
            'Technical Q&A Session', 
            'Creative Writing Help',
            'Code Review Discussion',
            'Product Planning Talk'
          ]
        }),
      }
    },
    messages: {
      count: 6,
      columns: {
        role: f.valuesFromArray({
          values: ['user', 'system']
        }),
        content: f.valuesFromArray({
          values: [
            'Hello! How can you help me today?',
            'I\'m an AI system, happy to help with various tasks!',
            'Can you explain how TypeScript interfaces work?',
            'TypeScript interfaces define the shape of objects...',
            'What are the best practices for React development?',
            'Some key React best practices include using functional components...',
          ]
        }),
      }
    }
  }));
  
  return db;
}

export async function resetDatabase(db: any) {
  await reset(db as any, schema);
}

6. テストの実装

tests/db.test.ts で実際のテストを実装します。このテストで、データベースの接続やクエリ、モデルの動作や挙動などが正しく機能するかどうかを検証します。

tests/db.test.ts

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { conversations, messages } from '../drizzle/schema';
import { eq } from 'drizzle-orm';
import { 
  createTestDatabase,
  seedTestDataWithDrizzleSeed, 
  resetDatabase 
} from './db';

describe('Drizzle Seed Test', () => {
  let client: any;
  let db: any;

  beforeEach(async () => {
    const testDb = await createTestDatabase();
    client = testDb.client;
    db = testDb.db;
  });

  afterEach(() => {
    client.close();
  });

  it('should seed test data using drizzle-seed', async () => {
    await seedTestDataWithDrizzleSeed(db, { count: 3, seed: 12345 });

    
    const allConversations = await db.select().from(conversations);
    expect(allConversations.length).toBeGreaterThan(0);

    
    const allMessages = await db.select().from(messages);
    expect(allMessages.length).toBeGreaterThan(0);

    
    for (const message of allMessages) {
      const conversation = allConversations.find((c: any) => 
        c.id === message.conversationId
      );
      expect(conversation).toBeDefined();
    }

    
    for (const message of allMessages) {
      expect(['user', 'system']).toContain(message.role);
    }
  });

  it('should reset database correctly', async () => {
    await seedTestDataWithDrizzleSeed(db);

    let allConversations = await db.select().from(conversations);
    expect(allConversations.length).toBeGreaterThan(0);

    await resetDatabase(db);

    allConversations = await db.select().from(conversations);
    const allMessages = await db.select().from(messages);

    expect(allConversations).toHaveLength(0);
    expect(allMessages).toHaveLength(0);
  });
});

予め package.jsonscripts へ下記を登録しておきます。

package.json

{
  "scripts": {
    "test": "vitest run",
    "db:gen": "drizzle-kit generate",
    "db:mgn": "drizzle-kit migrate",
    "db:mgn:prd": "NODE_ENV=production drizzle-kit migrate",
    "db:client": "drizzle-kit studio"
  }
}

1. DB のローカル環境構築


npx wrangler dev


npm run db:gen 
npm run db:mgn


npm test

Drizzle には、Drizzle Studio という GUI で DB を確認可能なツールが存在します。
Drizzle Studio は下記コマンドで起動可能です。

2. DB の本番環境への反映

正常にコマンド実行が成功すれば、Cloudflare Dashboard の「D1 SQL データベース」から /tables コマンドで生成されたテーブルが確認できるはずです。
コンソールから  でテーブルを確認

Drizzle のエコシステムを活用することで、OpenNext + Cloudflare D1 を使った開発・テスト・本番環境をシームレスに構築できました。

OpenNext を利用したい動機として、複雑な設定や環境構築に時間を取られることなく、迅速かつ効率的にアプリケーションの実装を進めたいというモチベで採用されることが大半だと考えます。

本記事の手法を使うことで、データベース周りの煩雑な設定や運用から解放されます。
そのため、アプリケーションロジックの実装に集中できます。爆速で開発を進めていきましょう。



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -