Documentation Drizzle

概要

この記事では、AWSのData APIを通じてDrizzle ORMでMySQLデータベースに接続し、サーバレス環境でデータ操作を行う方法を解説します。Drizzle ORMは型安全なORMで、AWSのData APIを活用することでサーバレス環境からMySQLデータベースへの接続が可能となり、効率的で安全なデータ管理が実現します。Data APIはクライアントライブラリなしでデータベース操作が行えるため、サーバレス環境でのデータベース利用に最適です。

AWSのData APIとDrizzle ORMの概要

AWS Data APIとは

AWS Data APIは、Amazon RDS上のMySQLやAurora ServerlessにアクセスできるAPIで、SQLクエリをHTTPリクエスト経由で実行する機能を提供します。以下の利点があります:

  • サーバレス対応:クライアントライブラリ不要で、AWS Lambdaなどのサーバレス環境から直接データベースにアクセスできる。
  • IAM認証でのセキュアなアクセス:安全な認証を提供し、アクセス制御が簡単。
  • 高いスケーラビリティ:オンデマンドでスケールするため、大規模なデータ操作にも対応。

Drizzle ORMとは

Drizzle ORMは、JavaScriptとTypeScriptで使用できる軽量なORMで、型安全なデータベース操作が可能です。Drizzle ORMはSQLに近い記述でクエリを操作でき、型定義が明確なためエラーの少ないコードが書けます。Data APIと組み合わせることで、AWSのサーバレス環境でも簡単かつ効率的にデータ操作が可能です。

AWS Data APIとDrizzle ORMでMySQLに接続する手順

AWS RDSでのMySQLインスタンス作成とData APIの有効化

まず、AWS RDSでMySQLデータベースインスタンスを作成し、Data APIを有効にします。

  1. AWS Management Consoleにログインし、RDSサービスで「データベースを作成」します。
  2. データベースエンジンとしてMySQLを選択。
  3. 「サーバレス」設定でインスタンスを作成し、設定完了後にData APIを有効化します。
  4. AWSによって生成されたData APIのエンドポイントURLやARN(Amazon Resource Name)を控えておきます。 これにより、AWSのData API経由で、HTTPリクエストを使ってサーバレス環境からMySQLに接続できるようになります。

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

Drizzle ORMとAWSのData APIに接続するために必要なパッケージをインストールします。@aws-sdk/client-rds-dataはData APIへのアクセスに使います。

npm install drizzle-orm @aws-sdk/client-rds-data
  • drizzle-orm:ORM本体で、型安全なクエリを提供します。
  • @aws-sdk/client-rds-data:AWS Data APIに接続するためのSDKです。

Drizzle ORMとAWS Data APIの接続設定

Drizzle ORMを利用してAWS Data API経由でMySQLに接続するため、クライアント設定を行います。以下の設定例を参考にdatabase.tsファイルを作成します。

// database.ts
import { drizzle } from 'drizzle-orm/aws-data-api/pg'; // PostgreSQLの場合
// または MySQL用のドライバを使用
import { RDSDataClient } from '@aws-sdk/client-rds-data';

// AWS Data API クライアントの設定
const rdsClient = new RDSDataClient({
  region: process.env.AWS_REGION || 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

// Drizzle ORM の初期化
export const db = drizzle(rdsClient, {
  database: process.env.DATABASE_NAME || 'mydb',
  resourceArn: process.env.DB_RESOURCE_ARN!,
  secretArn: process.env.DB_SECRET_ARN!,
});

// 環境変数の例(.env ファイル)
// AWS_REGION=us-east-1
// AWS_ACCESS_KEY_ID=your-access-key
// AWS_SECRET_ACCESS_KEY=your-secret-key
// DATABASE_NAME=employees_db
// DB_RESOURCE_ARN=arn:aws:rds:us-east-1:123456789012:cluster:my-cluster
// DB_SECRET_ARN=arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials-AbCdEf

接続情報の詳細:

  • region: MySQLデータベースのAWSリージョン(例: us-east-1, ap-northeast-1
  • database: MySQLのデータベース名
  • resourceArn: RDSクラスターまたはインスタンスのARN(AWS RDSコンソールから取得)
  • secretArn: データベース認証情報が保存されているAWS Secrets ManagerのARN

これにより、Drizzle ORMを介してAWS Data APIでMySQLにアクセスできるようになります。

テーブルのスキーマ定義

Drizzle ORMを用いてテーブルのスキーマを定義します。例えば、従業員情報を格納するemployeesテーブルの例を以下に示します。

// schema.ts
import { mysqlTable, serial, varchar, int, timestamp } from 'drizzle-orm/mysql-core';

// employees テーブルのスキーマ
export const employees = mysqlTable('employees', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  role: varchar('role', { length: 50 }).notNull(),
  salary: int('salary'),
  department: varchar('department', { length: 50 }),
  hire_date: timestamp('hire_date').defaultNow(),
  created_at: timestamp('created_at').defaultNow().notNull(),
  updated_at: timestamp('updated_at').defaultNow().onUpdateNow()
});

// 型定義の自動生成(TypeScript)
export type Employee = typeof employees.$inferSelect;
export type NewEmployee = typeof employees.$inferInsert;

このスキーマ定義では、以下の機能を実装しています:

  • serial('id'): 自動インクリメントの主キー
  • varchar(): 文字列型(長さ指定)
  • int(): 整数型(給与)
  • timestamp(): 日時型(自動更新機能付き)
  • 型推論による型安全性($inferSelect, $inferInsert

データ操作 - 挿入、取得、更新、削除

Drizzle ORMを利用して、AWS Data API経由でMySQLに対するデータ操作を行います。以下に基本的なCRUD操作のサンプルコードを示します。

データの挿入

import { db } from './database';
import { employees, type NewEmployee } from './schema';

// 単一レコードの挿入
async function insertEmployee(data: NewEmployee) {
  const result = await db.insert(employees).values(data);
  return result;
}

// 使用例
await insertEmployee({
  name: '田中太郎',
  email: 'tanaka@example.com',
  role: 'Engineer',
  salary: 5000000,
  department: 'Development'
});

// 複数レコードの一括挿入
async function insertMultipleEmployees(employeeList: NewEmployee[]) {
  await db.insert(employees).values(employeeList);
}

await insertMultipleEmployees([
  { name: '佐藤花子', email: 'sato@example.com', role: 'Designer', salary: 4500000 },
  { name: '鈴木一郎', email: 'suzuki@example.com', role: 'Manager', salary: 6000000 }
]);

データの取得

import { eq, and, gte, like } from 'drizzle-orm';

// 全件取得
async function getAllEmployees() {
  const result = await db.select().from(employees);
  return result;
}

// 条件付き取得
async function getEmployeeById(id: number) {
  const result = await db
    .select()
    .from(employees)
    .where(eq(employees.id, id))
    .limit(1);
  return result[0];
}

// 複雑な条件での検索
async function searchEmployees(department: string, minSalary: number) {
  return await db
    .select()
    .from(employees)
    .where(
      and(
        eq(employees.department, department),
        gte(employees.salary, minSalary)
      )
    )
    .orderBy(employees.salary);
}

// 部分一致検索
async function findEmployeesByName(namePart: string) {
  return await db
    .select()
    .from(employees)
    .where(like(employees.name, `%${namePart}%`));
}

データの更新

// 単一レコードの更新
async function updateEmployeeRole(id: number, newRole: string) {
  await db
    .update(employees)
    .set({ role: newRole })
    .where(eq(employees.id, id));
}

// 複数フィールドの更新
async function updateEmployee(id: number, data: Partial<NewEmployee>) {
  await db
    .update(employees)
    .set(data)
    .where(eq(employees.id, id));
}

// 使用例
await updateEmployee(1, {
  role: 'Senior Engineer',
  salary: 5500000,
  department: 'Architecture'
});

// 条件付き一括更新
async function giveRaiseByDepartment(department: string, raiseAmount: number) {
  await db
    .update(employees)
    .set({ salary: sql`${employees.salary} + ${raiseAmount}` })
    .where(eq(employees.department, department));
}

データの削除

// 単一レコードの削除
async function deleteEmployee(id: number) {
  await db
    .delete(employees)
    .where(eq(employees.id, id));
}

// 条件付き削除
async function deleteInactiveEmployees(beforeDate: Date) {
  await db
    .delete(employees)
    .where(and(
      eq(employees.role, 'Inactive'),
      gte(employees.updated_at, beforeDate)
    ));
}

AWS LambdaやAPI Gatewayとの統合

AWSのData APIはサーバレス環境での利用を前提としているため、Lambda関数でこれらのデータ操作を実行することが推奨されます。以下は、Lambda関数を用いたRESTful APIの実装例です。

// lambda-handler.ts
import { APIGatewayProxyHandler, APIGatewayProxyEvent } from 'aws-lambda';
import { db } from './database';
import { employees, type NewEmployee } from './schema';
import { eq } from 'drizzle-orm';

// GET /employees - 全従業員取得
export const listEmployees: APIGatewayProxyHandler = async () => {
  try {
    const employeesList = await db.select().from(employees);

    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        success: true,
        data: employeesList,
        count: employeesList.length
      }),
    };
  } catch (error) {
    console.error('Error fetching employees:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        success: false,
        error: 'Failed to fetch employees'
      }),
    };
  }
};

// GET /employees/{id} - 特定従業員取得
export const getEmployee: APIGatewayProxyHandler = async (event) => {
  try {
    const id = parseInt(event.pathParameters?.id || '0');

    if (!id) {
      return {
        statusCode: 400,
        body: JSON.stringify({ success: false, error: 'Invalid ID' }),
      };
    }

    const employee = await db
      .select()
      .from(employees)
      .where(eq(employees.id, id))
      .limit(1);

    if (employee.length === 0) {
      return {
        statusCode: 404,
        body: JSON.stringify({ success: false, error: 'Employee not found' }),
      };
    }

    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ success: true, data: employee[0] }),
    };
  } catch (error) {
    console.error('Error fetching employee:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ success: false, error: 'Internal server error' }),
    };
  }
};

// POST /employees - 新規従業員作成
export const createEmployee: APIGatewayProxyHandler = async (event) => {
  try {
    if (!event.body) {
      return {
        statusCode: 400,
        body: JSON.stringify({ success: false, error: 'Request body is required' }),
      };
    }

    const data: NewEmployee = JSON.parse(event.body);

    // バリデーション
    if (!data.name || !data.email || !data.role) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          success: false,
          error: 'Name, email, and role are required'
        }),
      };
    }

    const result = await db.insert(employees).values(data);

    return {
      statusCode: 201,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        success: true,
        message: 'Employee created successfully',
        data: result
      }),
    };
  } catch (error) {
    console.error('Error creating employee:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ success: false, error: 'Failed to create employee' }),
    };
  }
};

// PUT /employees/{id} - 従業員情報更新
export const updateEmployee: APIGatewayProxyHandler = async (event) => {
  try {
    const id = parseInt(event.pathParameters?.id || '0');

    if (!id || !event.body) {
      return {
        statusCode: 400,
        body: JSON.stringify({ success: false, error: 'Invalid request' }),
      };
    }

    const data: Partial<NewEmployee> = JSON.parse(event.body);

    await db
      .update(employees)
      .set(data)
      .where(eq(employees.id, id));

    return {
      statusCode: 200,
      body: JSON.stringify({
        success: true,
        message: 'Employee updated successfully'
      }),
    };
  } catch (error) {
    console.error('Error updating employee:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ success: false, error: 'Failed to update employee' }),
    };
  }
};

// DELETE /employees/{id} - 従業員削除
export const deleteEmployee: APIGatewayProxyHandler = async (event) => {
  try {
    const id = parseInt(event.pathParameters?.id || '0');

    if (!id) {
      return {
        statusCode: 400,
        body: JSON.stringify({ success: false, error: 'Invalid ID' }),
      };
    }

    await db
      .delete(employees)
      .where(eq(employees.id, id));

    return {
      statusCode: 200,
      body: JSON.stringify({
        success: true,
        message: 'Employee deleted successfully'
      }),
    };
  } catch (error) {
    console.error('Error deleting employee:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ success: false, error: 'Failed to delete employee' }),
    };
  }
};

このコードをAWS Lambdaにデプロイし、API Gatewayを設定することで、HTTPリクエストを受けてMySQLデータベースにアクセスできるRESTful APIエンドポイントが完成します。

IAMポリシーの設定

AWS Data APIを使用するには、Lambda関数に適切なIAMロールとポリシーを設定する必要があります。以下は、必要な権限を含むIAMポリシーの例です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "rds-data:ExecuteStatement",
        "rds-data:BatchExecuteStatement",
        "rds-data:BeginTransaction",
        "rds-data:CommitTransaction",
        "rds-data:RollbackTransaction"
      ],
      "Resource": "arn:aws:rds:us-east-1:123456789012:cluster:my-cluster"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials-*"
    }
  ]
}

このポリシーをLambda実行ロールにアタッチすることで、以下の権限が付与されます:

  • rds-data:ExecuteStatement: SQLクエリの実行
  • rds-data:BatchExecuteStatement: バッチクエリの実行
  • rds-data:BeginTransaction: トランザクションの開始
  • rds-data:CommitTransaction: トランザクションのコミット
  • rds-data:RollbackTransaction: トランザクションのロールバック
  • secretsmanager:GetSecretValue: データベース認証情報の取得

serverless.ymlでのデプロイ設定例

Serverless Frameworkを使用したデプロイ設定の例です。

service: employee-api

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  environment:
    DATABASE_NAME: ${env:DATABASE_NAME}
    DB_RESOURCE_ARN: ${env:DB_RESOURCE_ARN}
    DB_SECRET_ARN: ${env:DB_SECRET_ARN}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - rds-data:ExecuteStatement
            - rds-data:BatchExecuteStatement
            - rds-data:BeginTransaction
            - rds-data:CommitTransaction
            - rds-data:RollbackTransaction
          Resource: ${env:DB_RESOURCE_ARN}
        - Effect: Allow
          Action:
            - secretsmanager:GetSecretValue
          Resource: ${env:DB_SECRET_ARN}

functions:
  listEmployees:
    handler: src/lambda-handler.listEmployees
    events:
      - httpApi:
          path: /employees
          method: get

  getEmployee:
    handler: src/lambda-handler.getEmployee
    events:
      - httpApi:
          path: /employees/{id}
          method: get

  createEmployee:
    handler: src/lambda-handler.createEmployee
    events:
      - httpApi:
          path: /employees
          method: post

  updateEmployee:
    handler: src/lambda-handler.updateEmployee
    events:
      - httpApi:
          path: /employees/{id}
          method: put

  deleteEmployee:
    handler: src/lambda-handler.deleteEmployee
    events:
      - httpApi:
          path: /employees/{id}
          method: delete

エラー対処と注意点

1. Data API接続エラー

問題: ResourceNotFoundException や接続タイムアウトエラーが発生する

対処法:

  • リージョン、データベース名、ARNが正しく設定されているか確認
  • 環境変数が正しく設定されているか確認
  • RDSクラスターがData APIを有効化しているか確認
// デバッグ用のログ出力
console.log('DB Resource ARN:', process.env.DB_RESOURCE_ARN);
console.log('DB Secret ARN:', process.env.DB_SECRET_ARN);
console.log('Database Name:', process.env.DATABASE_NAME);

2. IAMアクセス権限エラー

問題: AccessDeniedException が発生する

対処法:

  • Lambda実行ロールに以下の権限が付与されているか確認:
    • rds-data:ExecuteStatement
    • rds-data:BatchExecuteStatement
    • secretsmanager:GetSecretValue
  • ポリシーのリソースARNが正しいか確認
  • Secrets Managerへのアクセス権限が付与されているか確認

3. トランザクションのタイムアウト

問題: 長時間実行されるトランザクションがタイムアウトする

対処法:

// トランザクションタイムアウトの設定
import { db } from './database';

await db.transaction(async (tx) => {
  // 複数の操作を実行
  await tx.insert(employees).values({ /* ... */ });
  await tx.update(employees).set({ /* ... */ });
}, {
  timeout: 30000 // 30秒(デフォルトは10秒)
});

4. 型エラーとバリデーション

問題: スキーマに一致しないデータを挿入しようとしてエラーが発生

対処法:

import { z } from 'zod';

// Zodスキーマでバリデーション
const employeeSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email().max(255),
  role: z.string().min(1).max(50),
  salary: z.number().int().positive().optional(),
  department: z.string().max(50).optional()
});

export const createEmployee: APIGatewayProxyHandler = async (event) => {
  try {
    const data = JSON.parse(event.body || '{}');

    // バリデーション
    const validated = employeeSchema.parse(data);

    await db.insert(employees).values(validated);

    return {
      statusCode: 201,
      body: JSON.stringify({ success: true })
    };
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          success: false,
          error: 'Validation failed',
          details: error.errors
        })
      };
    }
    // その他のエラー処理
  }
};

5. コールドスタートとコネクション管理

注意点: Data APIはHTTPベースのため、従来のコネクションプールは不要ですが、Lambda関数のコールドスタートを考慮してください。

ベストプラクティス:

  • データベース接続は関数の外部で初期化する
  • Lambda関数のメモリとタイムアウトを適切に設定する
  • 頻繁にアクセスされるエンドポイントにはProvisioned Concurrencyを検討する

まとめ

Drizzle ORMとAWSのData APIを組み合わせることで、型安全かつ効率的なサーバレス環境でのMySQLデータベース操作が可能になります。AWSのData APIはHTTPリクエストでデータベース操作が行えるため、LambdaやAPI Gatewayとの連携が容易で、スケーラブルでセキュアなデータ管理が実現します。Drizzle ORMを利用することで、シンプルで型安全なデータ操作を提供し、モダンなサーバレスアーキテクチャに最適な構成が可能です。

主な利点:

  • 型安全性: TypeScriptとの完全な統合により、コンパイル時に型エラーを検出
  • サーバレス対応: コネクションプール不要で、Lambda関数での利用に最適
  • セキュリティ: IAM認証とSecrets Managerによる安全な認証情報管理
  • スケーラビリティ: AWS Data APIの自動スケーリングにより、負荷に応じた処理が可能
  • 開発効率: 直感的なAPIと充実した型推論により、開発速度が向上

参考文献

円