Next.js でデータベースを扱う際、どの ORM とデータベースを選ぶかは重要な決定です。本記事では、Turso(エッジ最適化された SQLite データベース)と Drizzle ORM の組み合わせを推奨し、その導入方法をステップバイステップで解説します。
なぜ Turso + Drizzle ORM なのか
Turso とは
Turso は libSQL をベースにしたエッジ最適化された SQLite データベースサービスです。
- グローバル分散: エッジロケーションにデータをレプリケート
- 低レイテンシ: ユーザーに近い場所からデータを提供
- SQLite 互換: 使い慣れた SQL 構文をそのまま使用可能
- Embedded Replicas: ローカルにレプリカを持ち、マイクロ秒レベルの読み取りを実現
- 無料枠が充実: 個人プロジェクトや小規模アプリに最適
Drizzle ORM を選ぶ理由
Turso と組み合わせる ORM として、Drizzle ORM を強く推奨します。
| 特徴 | Drizzle ORM | Prisma |
|---|---|---|
| Turso サポート | ネイティブサポート | アダプター経由 |
| バンドルサイズ | 軽量(〜50KB) | 大きい(〜2MB) |
| Edge Runtime | 完全対応 | 制限あり |
| 型安全性 | TypeScript ファースト | 生成された型 |
| マイグレーション | Drizzle Kit 内蔵 | Prisma Migrate |
| クエリ構文 | SQL に近い | 独自 DSL |
Drizzle ORM は Turso/libSQL にネイティブ対応しており、Edge Runtime での動作も優れています。
ステップ1: Turso CLI のインストールとセットアップ
Turso CLI のインストール
# macOS (Homebrew)
brew install tursodatabase/tap/turso
# Linux / WSL
curl -sSfL https://get.tur.so/install.sh | bash
# Windows (PowerShell)
iwr -useb https://get.tur.so/install.ps1 | iexTurso にログイン
turso auth loginブラウザが開き、GitHub アカウントでの認証が求められます。
データベースの作成
# データベースを作成(名前は任意)
turso db create my-nextjs-app
# 作成されたデータベースの確認
turso db list認証情報の取得
# データベース URL の取得
turso db show --url my-nextjs-app
# 認証トークンの作成
turso db tokens create my-nextjs-app出力された URL とトークンを控えておいてください。
ステップ2: プロジェクトへのパッケージインストール
必要なパッケージのインストール
# npm
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
# pnpm
pnpm add drizzle-orm @libsql/client
pnpm add -D drizzle-kit
# bun
bun add drizzle-orm @libsql/client
bun add -D drizzle-kit環境変数の設定
.env.local ファイルを作成(または編集)します:
# Turso Database
TURSO_DATABASE_URL=libsql://your-database-org.turso.io
TURSO_AUTH_TOKEN=your-auth-token-here注意: .env.local を .gitignore に追加してください。
ステップ3: データベース接続の設定
libSQL クライアントの設定
src/db/index.ts を作成します:
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";
const tursoUrl = process.env.TURSO_DATABASE_URL;
const tursoAuthToken = process.env.TURSO_AUTH_TOKEN;
if (!tursoUrl || !tursoAuthToken) {
throw new Error("TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set.");
}
const client = createClient({
url: tursoUrl,
authToken: tursoAuthToken,
});
export const db = drizzle(client, { schema });Embedded Replicas を使用する場合(オプション)
ローカルにレプリカを持ち、読み取りを高速化する設定です:
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";
const client = createClient({
url: "file:local-replica.db", // ローカルファイル
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60, // 60秒ごとに自動同期
});
export const db = drizzle(client, { schema });
// 手動で同期する場合
export async function syncDatabase() {
await client.sync();
}ステップ4: スキーマの定義
基本的なスキーマファイル
src/db/schema.ts を作成します:
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
// ユーザーテーブル
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: text("created_at")
.notNull()
.default(sql`(datetime('now'))`),
updatedAt: text("updated_at")
.notNull()
.default(sql`(datetime('now'))`),
});
// 投稿テーブル
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content"),
authorId: integer("author_id")
.notNull()
.references(() => users.id),
published: integer("published", { mode: "boolean" }).default(false),
createdAt: text("created_at")
.notNull()
.default(sql`(datetime('now'))`),
});
// 型のエクスポート
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;リレーションの定義
src/db/relations.ts を作成します:
import { relations } from "drizzle-orm";
import { users, posts } from "./schema";
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));スキーマファイルでリレーションをエクスポートします:
// src/db/schema.ts に追加
export * from "./relations";ステップ5: Drizzle Kit の設定
設定ファイルの作成
プロジェクトルートに drizzle.config.ts を作成します:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "turso",
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
},
});package.json にスクリプトを追加
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
}
}ステップ6: マイグレーションの実行
マイグレーションファイルの生成と実行
# マイグレーションファイルを生成
npm run db:generate
# マイグレーションを実行
npm run db:migrate生成されたマイグレーションファイルは drizzle/ ディレクトリに保存されます。
Drizzle Studio でデータを確認
npm run db:studioブラウザでデータベースの内容を GUI で確認・編集できます。
ステップ7: Next.js での使用例
App Router でのデータ取得
src/app/users/page.tsx:
import { db } from "@/db";
import { users } from "@/db/schema";
export default async function UsersPage() {
const allUsers = await db.select().from(users);
return (
<div>
<h1>ユーザー一覧</h1>
<ul>
{allUsers.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}リレーションを含むクエリ
import { db } from "@/db";
import { posts, users } from "@/db/schema";
import { eq } from "drizzle-orm";
export default async function PostsPage() {
// リレーションクエリを使用
const postsWithAuthors = await db.query.posts.findMany({
with: {
author: true,
},
});
// または JOIN を使用
const result = await db
.select({
post: posts,
author: users,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
return (
<div>
<h1>投稿一覧</h1>
{postsWithAuthors.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>著者: {post.author.name}</p>
</article>
))}
</div>
);
}Server Actions でのデータ操作
src/app/users/actions.ts:
"use server";
import { db } from "@/db";
import { users, type NewUser } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
export async function createUser(data: NewUser) {
const result = await db.insert(users).values(data).returning();
revalidatePath("/users");
return result[0];
}
export async function updateUser(id: number, data: Partial<NewUser>) {
const result = await db
.update(users)
.set(data)
.where(eq(users.id, id))
.returning();
revalidatePath("/users");
return result[0];
}
export async function deleteUser(id: number) {
await db.delete(users).where(eq(users.id, id));
revalidatePath("/users");
}API Route での使用
src/app/api/users/route.ts:
import { db } from "@/db";
import { users } from "@/db/schema";
import { NextResponse } from "next/server";
export async function GET() {
const allUsers = await db.select().from(users);
return NextResponse.json(allUsers);
}
export async function POST(request: Request) {
const body = await request.json();
const newUser = await db
.insert(users)
.values({
name: body.name,
email: body.email,
})
.returning();
return NextResponse.json(newUser[0], { status: 201 });
}ステップ8: Edge Runtime での使用
Edge Function の設定
src/app/api/edge-users/route.ts:
import { db } from "@/db";
import { users } from "@/db/schema";
import { NextResponse } from "next/server";
export const runtime = "edge";
export async function GET() {
const allUsers = await db.select().from(users);
return NextResponse.json(allUsers);
}Turso + Drizzle の組み合わせは Edge Runtime で完全に動作します。
ステップ9: 高度なクエリパターン
フィルタリングと並び替え
import { db } from "@/db";
import { posts } from "@/db/schema";
import { eq, and, or, desc, like } from "drizzle-orm";
// 公開済みの投稿を新しい順に取得
const publishedPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
// 複合条件
const filteredPosts = await db
.select()
.from(posts)
.where(
and(
eq(posts.published, true),
or(
like(posts.title, "%Next.js%"),
like(posts.content, "%TypeScript%")
)
)
);ページネーション
import { db } from "@/db";
import { posts } from "@/db/schema";
import { desc } from "drizzle-orm";
async function getPaginatedPosts(page: number, pageSize: number = 10) {
const offset = (page - 1) * pageSize;
const results = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(pageSize)
.offset(offset);
return results;
}トランザクション
import { db } from "@/db";
import { users, posts } from "@/db/schema";
async function createUserWithPost(
userData: NewUser,
postData: Omit<NewPost, "authorId">
) {
return await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values(userData).returning();
const [post] = await tx
.insert(posts)
.values({
...postData,
authorId: user.id,
})
.returning();
return { user, post };
});
}プロジェクト構成のまとめ
src/
├── app/
│ ├── api/
│ │ └── users/
│ │ └── route.ts
│ ├── users/
│ │ ├── page.tsx
│ │ └── actions.ts
│ └── layout.tsx
├── db/
│ ├── index.ts # DB接続設定
│ ├── schema.ts # テーブル定義
│ └── relations.ts # リレーション定義
drizzle/
├── 0000_*.sql # マイグレーションファイル
└── meta/
drizzle.config.ts # Drizzle Kit 設定
.env.local # 環境変数トラブルシューティング
接続エラーが発生する
環境変数が正しく設定されているか確認してください:
# 環境変数の確認
echo $TURSO_DATABASE_URL
echo $TURSO_AUTH_TOKENマイグレーションが失敗する
Turso のデータベースに接続できるか確認:
turso db shell my-nextjs-appEdge Runtime でエラーが発生する
@libsql/client の Web 互換バージョンを使用していることを確認してください。最新版では自動的に適切なバージョンが選択されます。
まとめ
Turso + Drizzle ORM の組み合わせは、Next.js プロジェクトに最適なデータベースソリューションです:
- Turso: エッジ最適化された SQLite で低レイテンシを実現
- Drizzle ORM: 軽量で型安全、Turso にネイティブ対応
- Edge Runtime 対応: Vercel Edge Functions などで完全動作
- 開発体験: Drizzle Studio による GUI でのデータ管理
この組み合わせにより、型安全で高速なデータベース操作を実現できます。