概要
この記事では、Drizzle ORMを用いてAWSのData API経由でPostgreSQLデータベースに接続し、サーバレス環境でデータ操作を行う方法を詳しく解説します。Drizzle ORMは、JavaScriptやTypeScriptで型安全なデータベース操作を提供し、AWSのData APIを利用することで、サーバレスアーキテクチャでも簡単にPostgreSQLにアクセスできます。Data APIは、AWSのRDSにおいてクライアントライブラリを使用せずにHTTPリクエストを介してデータベース操作ができる仕組みです。
AWSのData APIとDrizzle ORMの概要
AWS Data APIとは
AWS Data APIは、RDS(Relational Database Service)上のPostgreSQLおよびAmazon Auroraサーバレスインスタンスに対して、HTTPリクエストを介してデータ操作を行うAPIです。これにより、以下のような利点が得られます:
- サーバレス対応:クライアントライブラリなしでデータベースにアクセスでき、AWS Lambdaなどのサーバレス環境で利用しやすい。
- セキュアなアクセス:IAM認証で安全にデータベースにアクセス。
- スケーラビリティ:オンデマンドでデータベースにアクセスするため、高いスケーラビリティを実現。
Drizzle ORMとは
Drizzle ORMは、JavaScriptやTypeScriptで利用できる軽量で型安全なORMです。Drizzle ORMはSQLに似た直感的なクエリ構文を提供し、TypeScriptの型を活用して安全にデータベース操作を行うことができます。AWSのData APIと組み合わせることで、サーバレス環境でも効率的にデータ管理が可能です。
AWS Data APIとDrizzle ORMの接続手順
AWS環境の設定 - RDSインスタンスの作成とData APIの有効化
まず、AWS RDSでPostgreSQLインスタンスを作成し、Data APIを有効にする必要があります。
- AWS Management Consoleにログイン。
- RDSサービスから新規データベースを作成し、データベースエンジンとしてPostgreSQLを選択。
- 作成画面で「サーバレス」モードを選び、インスタンス設定を完了します。
- データベース作成後、Data APIを有効にします。 この設定により、AWSのData APIを使ってHTTPリクエストでデータベースにアクセスできるようになります。Data APIのエンドポイントURLやARN(Amazon Resource Name)を控えておきましょう。
必要なパッケージのインストール
Drizzle ORMとAWS SDKを利用してData APIに接続するため、drizzle-ormと@aws-sdk/client-rds-dataをインストールします。
npm install drizzle-orm @aws-sdk/client-rds-data
drizzle-ormは型安全なORMで、クエリの記述を簡単に行えます。@aws-sdk/client-rds-dataはAWS Data APIにアクセスするためのAWS SDKクライアントです。
Drizzle ORMとData APIの接続設定
Drizzle ORMを使用してAWS Data API経由でPostgreSQLに接続するために、必要な設定を行います。以下に、接続設定の例を示します。
// database.ts
import { drizzle } from 'drizzle-orm/aws-data-api/pg';
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 の初期化(PostgreSQL用)
export const db = drizzle(rdsClient, {
database: process.env.DATABASE_NAME || 'postgres',
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-aurora-cluster
// DB_SECRET_ARN=arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials-AbCdEf
接続情報の詳細:
- region: PostgreSQLデータベースのAWSリージョン(例:
us-east-1,ap-northeast-1) - database: PostgreSQLのデータベース名
- resourceArn: Aurora PostgreSQLクラスターのARN(AWS RDSコンソールから取得)
- secretArn: データベース認証情報が保存されているAWS Secrets ManagerのARN
重要: AWS Data APIは主にAurora ServerlessおよびAurora Serverless v2で利用できます。通常のRDS PostgreSQLインスタンスでは使用できません。
これにより、Drizzle ORM経由でData APIを使用してPostgreSQLに接続できます。
テーブルのスキーマ定義
Drizzle ORMを使用して、テーブルのスキーマを定義します。以下は、employeesというテーブルのスキーマ例です。
// schema.ts
import { pgTable, serial, varchar, integer, timestamp, boolean, text } from 'drizzle-orm/pg-core';
// employees テーブルの定義
export const employees = pgTable('employees', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
position: varchar('position', { length: 50 }).notNull(),
salary: integer('salary'),
department: varchar('department', { length: 50 }),
is_active: boolean('is_active').default(true).notNull(),
bio: text('bio'),
hire_date: timestamp('hire_date', { mode: 'date' }).defaultNow(),
created_at: timestamp('created_at', { mode: 'date', withTimezone: true }).defaultNow().notNull(),
updated_at: timestamp('updated_at', { mode: 'date', withTimezone: true }).defaultNow().$onUpdate(() => new Date())
});
// 型定義の自動生成(TypeScript)
export type Employee = typeof employees.$inferSelect;
export type NewEmployee = typeof employees.$inferInsert;
このスキーマ定義では、以下の機能を実装しています:
serial('id'): 自動インクリメントの主キー(PostgreSQLのSERIAL型)varchar(): 可変長文字列型(長さ指定)integer(): 整数型(給与)boolean(): 真偽値型(アクティブ状態)text(): 長いテキスト型(自己紹介)timestamp(): タイムスタンプ型(タイムゾーン付き、自動更新機能付き)- 型推論による型安全性(
$inferSelect,$inferInsert)
データ操作 - 挿入、取得、更新、削除
Drizzle ORMを使ってData API経由でPostgreSQLにデータ操作を行います。以下に、基本的なCRUD操作の例を示します。
データの挿入
import { db } from './database';
import { employees, type NewEmployee } from './schema';
// 単一レコードの挿入
async function insertEmployee(data: NewEmployee) {
const result = await db.insert(employees).values(data).returning();
return result[0];
}
// 使用例
const newEmployee = await insertEmployee({
name: '山田太郎',
email: 'yamada@example.com',
position: 'Software Engineer',
salary: 6000000,
department: 'Engineering'
});
console.log('Created employee:', newEmployee);
// 複数レコードの一括挿入
async function insertMultipleEmployees(employeeList: NewEmployee[]) {
const result = await db.insert(employees).values(employeeList).returning();
return result;
}
await insertMultipleEmployees([
{ name: '佐藤花子', email: 'sato@example.com', position: 'Designer', salary: 5500000 },
{ name: '鈴木一郎', email: 'suzuki@example.com', position: 'Product Manager', salary: 7000000 }
]);
データの取得
import { eq, and, gte, like, desc, sql } from 'drizzle-orm';
// 全件取得
async function getAllEmployees() {
return await db.select().from(employees);
}
// 条件付き取得
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),
eq(employees.is_active, true)
)
)
.orderBy(desc(employees.salary));
}
// LIKE検索とページネーション
async function findEmployeesByName(namePart: string, page: number = 1, pageSize: number = 10) {
const offset = (page - 1) * pageSize;
return await db
.select()
.from(employees)
.where(like(employees.name, `%${namePart}%`))
.limit(pageSize)
.offset(offset);
}
// 集計クエリ
async function getEmployeeStats() {
const result = await db
.select({
department: employees.department,
count: sql<number>`count(*)`,
avgSalary: sql<number>`avg(${employees.salary})`,
maxSalary: sql<number>`max(${employees.salary})`
})
.from(employees)
.where(eq(employees.is_active, true))
.groupBy(employees.department);
return result;
}
データの更新
// 単一レコードの更新
async function updateEmployeePosition(id: number, newPosition: string) {
const result = await db
.update(employees)
.set({ position: newPosition })
.where(eq(employees.id, id))
.returning();
return result[0];
}
// 複数フィールドの更新
async function updateEmployee(id: number, data: Partial<NewEmployee>) {
const result = await db
.update(employees)
.set({
...data,
updated_at: new Date()
})
.where(eq(employees.id, id))
.returning();
return result[0];
}
// 使用例
await updateEmployee(1, {
position: 'Senior Engineer',
salary: 7500000,
department: 'Platform Engineering'
});
// 条件付き一括更新(昇給)
async function giveRaiseByDepartment(department: string, raisePercent: number) {
await db
.update(employees)
.set({
salary: sql`${employees.salary} * ${1 + raisePercent / 100}`,
updated_at: new Date()
})
.where(
and(
eq(employees.department, department),
eq(employees.is_active, true)
)
);
}
// 全従業員に5%昇給
await giveRaiseByDepartment('Engineering', 5);
データの削除
// 単一レコードの削除
async function deleteEmployee(id: number) {
const result = await db
.delete(employees)
.where(eq(employees.id, id))
.returning();
return result[0];
}
// 論理削除(is_activeをfalseに設定)
async function deactivateEmployee(id: number) {
return await db
.update(employees)
.set({ is_active: false, updated_at: new Date() })
.where(eq(employees.id, id))
.returning();
}
// 条件付き削除
async function deleteInactiveEmployees(daysInactive: number = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysInactive);
await db
.delete(employees)
.where(
and(
eq(employees.is_active, false),
sql`${employees.updated_at} < ${cutoffDate}`
)
);
}
AWS LambdaやAPI Gatewayと統合する
AWSのData APIは、サーバレス環境に適しているため、Lambda関数でこれらのデータ操作を実行することが推奨されます。以下は、Lambda関数を利用したRESTful APIの実装例です。
// handlers/employees.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { db } from '../database';
import { employees, type NewEmployee } from '../schema';
import { eq, like } from 'drizzle-orm';
// GET /employees - 全従業員取得(検索・ページネーション付き)
export const listEmployees: APIGatewayProxyHandler = async (event) => {
try {
const params = event.queryStringParameters || {};
const page = parseInt(params.page || '1');
const limit = parseInt(params.limit || '20');
const search = params.search;
const department = params.department;
let query = db.select().from(employees);
// 検索フィルタ
if (search) {
query = query.where(like(employees.name, `%${search}%`));
}
if (department) {
query = query.where(eq(employees.department, department));
}
// ページネーション
const offset = (page - 1) * limit;
const results = await query.limit(limit).offset(offset);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
data: results,
pagination: {
page,
limit,
count: results.length
}
}),
};
} catch (error) {
console.error('Error listing 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 employee 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.position) {
return {
statusCode: 400,
body: JSON.stringify({
success: false,
error: 'Name, email, and position are required'
}),
};
}
const result = await db
.insert(employees)
.values(data)
.returning();
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
message: 'Employee created successfully',
data: result[0]
}),
};
} catch (error: any) {
console.error('Error creating employee:', error);
// ユニーク制約違反のエラーハンドリング
if (error.code === '23505') {
return {
statusCode: 409,
body: JSON.stringify({
success: false,
error: 'Email already exists'
}),
};
}
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);
const result = await db
.update(employees)
.set({
...data,
updated_at: new Date()
})
.where(eq(employees.id, id))
.returning();
if (result.length === 0) {
return {
statusCode: 404,
body: JSON.stringify({ success: false, error: 'Employee not found' }),
};
}
return {
statusCode: 200,
body: JSON.stringify({
success: true,
message: 'Employee updated successfully',
data: result[0]
}),
};
} 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' }),
};
}
// 論理削除(is_activeをfalseに設定)
const result = await db
.update(employees)
.set({ is_active: false, updated_at: new Date() })
.where(eq(employees.id, id))
.returning();
if (result.length === 0) {
return {
statusCode: 404,
body: JSON.stringify({ success: false, error: 'Employee not found' }),
};
}
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' }),
};
}
};
このコードをデプロイすれば、Lambda経由でAPI GatewayからHTTPリクエストに応じてPostgreSQLにアクセスできるRESTful APIエンドポイントが構築されます。
IAMポリシーの設定
AWS Data APIを使用するには、Lambda関数に適切な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-aurora-cluster"
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rds-db-credentials-*"
}
]
}
トランザクションの使用例
PostgreSQLでの複雑な操作にはトランザクションを使用します。
import { db } from './database';
import { employees } from './schema';
async function transferEmployee(
employeeId: number,
newDepartment: string,
newSalary: number
) {
try {
// トランザクション開始
await db.transaction(async (tx) => {
// 従業員情報を更新
await tx
.update(employees)
.set({
department: newDepartment,
salary: newSalary,
updated_at: new Date()
})
.where(eq(employees.id, employeeId));
// ログテーブルに記録(例)
// await tx.insert(employeeLog).values({ ... });
// すべての操作が成功したらコミット
});
return { success: true, message: 'Transfer completed' };
} catch (error) {
// エラーが発生したら自動的にロールバック
console.error('Transaction failed:', error);
return { success: false, error: 'Transfer failed' };
}
}
エラー対処と注意点
1. Aurora Serverlessの制限
重要: AWS Data APIは以下のデータベースでのみ利用可能です:
- Amazon Aurora PostgreSQL Serverless v1
- Amazon Aurora PostgreSQL Serverless v2
- Amazon Aurora PostgreSQL Provisioned(Data API有効化が必要)
通常のRDS PostgreSQLインスタンスでは使用できません。
2. Data API接続エラー
問題: ResourceNotFoundException や BadRequestException が発生する
対処法:
// 接続情報のデバッグ
console.log('Configuration:', {
region: process.env.AWS_REGION,
database: process.env.DATABASE_NAME,
resourceArn: process.env.DB_RESOURCE_ARN?.substring(0, 50) + '...',
secretArn: process.env.DB_SECRET_ARN?.substring(0, 50) + '...'
});
// エラーハンドリング
try {
const result = await db.select().from(employees);
} catch (error: any) {
if (error.name === 'ResourceNotFoundException') {
console.error('Database cluster or secret not found');
} else if (error.name === 'BadRequestException') {
console.error('Invalid SQL or parameters');
}
throw error;
}
3. IAMアクセス権限エラー
問題: AccessDeniedException が発生する
対処法:
- Lambda実行ロールに以下の権限を確認:
rds-data:ExecuteStatementsecretsmanager:GetSecretValue
- ポリシーのリソースARNが正しいか確認
- Secrets ManagerのシークレットがData API用に適切に設定されているか確認
4. PostgreSQL固有のエラー
ユニーク制約違反(エラーコード: 23505):
try {
await db.insert(employees).values({ email: 'duplicate@example.com', ... });
} catch (error: any) {
if (error.code === '23505') {
// ユニーク制約違反の処理
return { error: 'Email already exists' };
}
}
外部キー制約違反(エラーコード: 23503):
if (error.code === '23503') {
return { error: 'Referenced record does not exist' };
}
5. データ型とnullエラー
問題: NOT NULL制約違反や型不一致
対処法:
import { z } from 'zod';
// Zodスキーマでバリデーション
const employeeSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email().max(255),
position: z.string().min(1).max(50),
salary: z.number().int().positive().optional(),
department: z.string().max(50).optional()
});
// Lambda関数内でバリデーション
try {
const validated = employeeSchema.parse(JSON.parse(event.body || '{}'));
await db.insert(employees).values(validated);
} catch (error) {
if (error instanceof z.ZodError) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Validation failed',
details: error.errors
})
};
}
}
6. タイムアウトとパフォーマンス
注意点:
- Data APIのタイムアウトはデフォルトで10秒
- Lambda関数のタイムアウトは適切に設定(推奨: 30秒以上)
- 大量データの処理にはバッチ処理を検討
// バッチ処理の例
async function batchInsertEmployees(employeeList: NewEmployee[]) {
const batchSize = 100;
for (let i = 0; i < employeeList.length; i += batchSize) {
const batch = employeeList.slice(i, i + batchSize);
await db.insert(employees).values(batch);
console.log(`Processed ${i + batch.length}/${employeeList.length}`);
}
}
7. ベストプラクティス
- 環境変数: 機密情報は環境変数で管理
- コネクション: データベース接続はLambda関数の外部で初期化
- エラーログ: CloudWatch Logsで詳細なエラー情報を記録
- 型安全性: TypeScriptの型推論を最大限活用
- テスト: ローカル開発にはAurora Serverless v2のローカルエンドポイントを使用
まとめ
Drizzle ORMとAWSのData APIを使うことで、サーバレス環境からPostgreSQLへの安全でスケーラブルなデータアクセスが可能です。Data APIはHTTPリクエストでデータベース操作ができるため、AWS LambdaやAPI Gatewayとの統合も簡単で、サーバレスアーキテクチャを最大限に活用したデータベース管理が実現します。Drizzle ORMの型安全な操作とAWSの高度なインフラを組み合わせることで、高パフォーマンスかつセキュアなデータ管理が可能です。
主な特徴:
- 型安全性: TypeScriptの完全な型推論により、コンパイル時にエラーを検出
- サーバレス最適化: コネクションプール不要で、Lambda関数に最適
- Aurora Serverless対応: オンデマンドスケーリングによる柔軟な容量管理
- IAM認証: Secrets Managerと統合したセキュアな認証
- トランザクションサポート: ACID特性を保証する堅牢なトランザクション処理
- PostgreSQL互換: 標準的なPostgreSQL機能をサポート