Turso データベース入門ガイド
Tursoは、SQLiteをベースにしたエッジ対応の分散データベースです。libSQLという拡張されたSQLiteフォークを使用し、グローバルに分散されたデータベースをローカルのSQLiteと同じように扱えます。
Tursoとは
Tursoは、以下の特徴を持つモダンなデータベースプラットフォームです。
主な特徴
- エッジ対応 - ユーザーに近い場所でデータベースを実行
- 分散レプリケーション - 複数のリージョンにデータを自動複製
- SQLite互換 - 既存のSQLiteツールとライブラリがそのまま使える
- 低レイテンシー - エッジでの実行により高速なレスポンス
- 従量課金 - 無料プランからスタート可能
libSQLの拡張機能
libSQLはSQLiteに以下の機能を追加しています。
- ネットワーク経由のアクセス - HTTP/WebSocket経由でのクエリ実行
- レプリケーション - マルチリージョンでのデータ同期
- ブランチング - データベースのブランチ作成(Git風)
- 埋め込みレプリカ - ローカルキャッシュとしての利用
セットアップ
Turso CLIのインストール
# macOS/Linux
curl -sSfL https://get.tur.so/install.sh | bash
# Windows (PowerShell)
irm get.tur.so/install.ps1 | iex
ログインと初期設定
# Tursoにログイン
turso auth login
# データベースを作成
turso db create my-database
# 接続情報を取得
turso db show my-database
# データベース一覧
turso db list
接続URLとトークンの取得
# データベースURL
turso db show my-database --url
# 認証トークン
turso db tokens create my-database
クライアントライブラリの使用
TypeScript/JavaScript
npm install @libsql/client
import { createClient } from "@libsql/client";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// テーブル作成
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// データ挿入
await client.execute({
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
args: ["Alice", "alice@example.com"],
});
// データ取得
const result = await client.execute("SELECT * FROM users");
console.log(result.rows);
// トランザクション
const tx = await client.transaction("write");
try {
await tx.execute({
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
args: ["Bob", "bob@example.com"],
});
await tx.execute({
sql: "UPDATE users SET name = ? WHERE email = ?",
args: ["Robert", "bob@example.com"],
});
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
}
バッチ処理
複数のクエリを効率的に実行できます。
const batch = [
{
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
args: ["Charlie", "charlie@example.com"],
},
{
sql: "INSERT INTO users (name, email) VALUES (?, ?)",
args: ["David", "david@example.com"],
},
{
sql: "UPDATE users SET name = ? WHERE id = ?",
args: ["Charles", 3],
},
];
await client.batch(batch);
Prepared Statements
const stmt = await client.prepare(
"SELECT * FROM users WHERE email = ?"
);
const result1 = await stmt.execute(["alice@example.com"]);
const result2 = await stmt.execute(["bob@example.com"]);
// 使用後はクリーンアップ
await stmt.finalize();
Drizzle ORMとの連携
Drizzle ORMを使うことで、型安全なデータベース操作が可能です。
セットアップ
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit
スキーマ定義
// 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: integer("created_at", { mode: "timestamp" })
.default(sql`CURRENT_TIMESTAMP`),
});
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
content: text("content").notNull(),
userId: integer("user_id")
.notNull()
.references(() => users.id),
published: integer("published", { mode: "boolean" }).default(false),
createdAt: integer("created_at", { mode: "timestamp" })
.default(sql`CURRENT_TIMESTAMP`),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
データベース接続
// db/client.ts
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const db = drizzle(client, { schema });
CRUD操作
import { db } from "./db/client";
import { users, posts } from "./db/schema";
import { eq, and, like, desc } from "drizzle-orm";
// 作成 (Create)
const newUser = await db.insert(users).values({
name: "Alice",
email: "alice@example.com",
}).returning();
// 読み取り (Read)
const allUsers = await db.select().from(users);
const user = await db.select()
.from(users)
.where(eq(users.email, "alice@example.com"))
.limit(1);
// ユーザーと投稿を結合
const usersWithPosts = await db.select({
user: users,
post: posts,
})
.from(users)
.leftJoin(posts, eq(users.id, posts.userId))
.where(eq(users.id, 1));
// 更新 (Update)
await db.update(users)
.set({ name: "Alice Smith" })
.where(eq(users.id, 1));
// 削除 (Delete)
await db.delete(users)
.where(eq(users.id, 1));
複雑なクエリ
// 検索
const searchResults = await db.select()
.from(posts)
.where(
and(
like(posts.title, "%TypeScript%"),
eq(posts.published, true)
)
)
.orderBy(desc(posts.createdAt))
.limit(10);
// 集計
import { count, avg } from "drizzle-orm";
const stats = await db.select({
userCount: count(),
avgPostsPerUser: avg(posts.userId),
})
.from(users)
.leftJoin(posts, eq(users.id, posts.userId));
// サブクエリ
const activeUsers = db.select({ id: users.id })
.from(users)
.where(eq(users.active, true));
const postsFromActiveUsers = await db.select()
.from(posts)
.where(inArray(posts.userId, activeUsers));
トランザクション
await db.transaction(async (tx) => {
const user = await tx.insert(users).values({
name: "Bob",
email: "bob@example.com",
}).returning();
await tx.insert(posts).values({
title: "First Post",
content: "Hello World",
userId: user[0].id,
published: true,
});
});
マイグレーション
Drizzle Kitの設定
// drizzle.config.ts
import type { Config } from "drizzle-kit";
export default {
schema: "./db/schema.ts",
out: "./drizzle",
driver: "turso",
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
} satisfies Config;
マイグレーションの生成と実行
# マイグレーションファイルを生成
npx drizzle-kit generate:sqlite
# マイグレーションを実行
npx drizzle-kit push:sqlite
# Drizzle Studio(GUIツール)を起動
npx drizzle-kit studio
Next.js との連携
APIルートでの使用
// app/api/users/route.ts
import { NextResponse } from "next/server";
import { db } from "@/db/client";
import { users } from "@/db/schema";
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 });
}
Server Componentsでの使用
// app/users/page.tsx
import { db } from "@/db/client";
import { users } from "@/db/schema";
export default async function UsersPage() {
const allUsers = await db.select().from(users);
return (
<div>
<h1>Users</h1>
<ul>
{allUsers.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}
Server Actionsでの使用
// app/actions.ts
"use server";
import { db } from "@/db/client";
import { users } from "@/db/schema";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await db.insert(users).values({ name, email });
revalidatePath("/users");
}
埋め込みレプリカ
ローカルにSQLiteファイルを持ち、Tursoと同期することでオフライン対応とパフォーマンス向上を実現できます。
import { createClient } from "@libsql/client";
const client = createClient({
url: "file:local.db", // ローカルファイル
syncUrl: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// 初回同期
await client.sync();
// ローカルで高速にクエリ実行
const users = await client.execute("SELECT * FROM users");
// 定期的に同期
setInterval(async () => {
await client.sync();
}, 60000); // 1分ごと
データベースブランチ
開発・ステージング・本番環境を簡単に分離できます。
# ブランチを作成
turso db create my-db-dev --from-db my-db
# ブランチ一覧
turso db list
# ブランチを削除
turso db destroy my-db-dev
パフォーマンス最適化
1. インデックスの作成
await client.execute(`
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
`);
2. クエリの最適化
// 悪い例: N+1クエリ
const users = await db.select().from(users);
for (const user of users) {
const posts = await db.select()
.from(posts)
.where(eq(posts.userId, user.id));
}
// 良い例: JOIN使用
const usersWithPosts = await db.select()
.from(users)
.leftJoin(posts, eq(users.id, posts.userId));
3. 接続プール
import { createClient } from "@libsql/client";
// 接続プールを設定
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
connectionPoolSize: 10, // プールサイズ
});
4. プリペアドステートメントの再利用
const stmt = await client.prepare(
"SELECT * FROM users WHERE id = ?"
);
// 複数回実行
for (const id of [1, 2, 3, 4, 5]) {
const result = await stmt.execute([id]);
console.log(result.rows);
}
await stmt.finalize();
セキュリティ
1. 環境変数の管理
// .env.local
TURSO_DATABASE_URL=libsql://your-database.turso.io
TURSO_AUTH_TOKEN=your-secret-token
// 環境変数のバリデーション
if (!process.env.TURSO_DATABASE_URL || !process.env.TURSO_AUTH_TOKEN) {
throw new Error("Missing Turso credentials");
}
2. SQLインジェクション対策
// 悪い例: 文字列結合
const email = req.query.email;
await client.execute(`SELECT * FROM users WHERE email = '${email}'`);
// 良い例: プレースホルダー使用
await client.execute({
sql: "SELECT * FROM users WHERE email = ?",
args: [email],
});
3. アクセス制御
// 読み取り専用トークンの作成
turso db tokens create my-database --read-only
// アプリケーション側で検証
const isAdmin = await checkUserRole(userId);
if (!isAdmin) {
throw new Error("Unauthorized");
}
モニタリングとデバッグ
クエリログ
import { drizzle } from "drizzle-orm/libsql";
const db = drizzle(client, {
schema,
logger: {
logQuery(query, params) {
console.log("Query:", query);
console.log("Params:", params);
},
},
});
データベース統計
# データベースの使用状況を確認
turso db inspect my-database
# 月間使用量
turso account usage
ベストプラクティス
1. スキーマ設計
// 正規化されたスキーマ
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
username: text("username").notNull().unique(),
email: text("email").notNull().unique(),
});
export const profiles = sqliteTable("profiles", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
bio: text("bio"),
avatar: text("avatar"),
});
2. エラーハンドリング
try {
await db.insert(users).values({
name: "Alice",
email: "alice@example.com",
});
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("UNIQUE constraint failed")) {
console.error("Email already exists");
} else {
console.error("Database error:", error.message);
}
}
throw error;
}
3. 型安全性の確保
import { InferModel } from "drizzle-orm";
type User = InferModel<typeof users, "select">;
type NewUser = InferModel<typeof users, "insert">;
function createUser(data: NewUser): Promise<User> {
return db.insert(users).values(data).returning();
}
まとめ
Tursoは、SQLiteの使いやすさとスケーラビリティを兼ね備えたモダンなデータベースソリューションです。
主な利点:
- エッジでの高速な実行
- SQLite互換による学習コストの低さ
- 分散レプリケーションによる可用性
- 無料プランからのスタート
- Drizzle ORMなどの充実したエコシステム
特に、グローバルに展開するアプリケーション、低レイテンシーが求められるサービス、Next.jsやRemixなどのモダンフレームワークとの組み合わせに最適です。埋め込みレプリカを使えば、オフライン対応も容易に実現できます。