Turso + LibSQL:エッジデータベース実践ガイド


TursoはLibSQLを基盤とするエッジデータベースサービスで、グローバルに分散されたSQLiteデータベースを提供します。本記事では、Tursoの実践的な使い方について詳しく解説します。

Turso + LibSQLとは

LibSQLの特徴

LibSQLは、SQLiteのフォークとして開発されたデータベースエンジンです。

  • SQLite互換: 既存のSQLite知識を活用可能
  • エッジ最適化: 低レイテンシーのグローバルアクセス
  • レプリケーション: マルチリージョンでのデータ同期
  • エンベデッド対応: サーバーレス環境で直接実行可能

Tursoの利点

// ✅ グローバルに低レイテンシー
const client = createClient({
  url: 'libsql://your-db.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN,
});

// ✅ エッジファンクションで直接クエリ
export default async function handler(req: Request) {
  const result = await client.execute('SELECT * FROM users WHERE id = ?', [1]);
  return new Response(JSON.stringify(result.rows));
}

セットアップ

Turso CLIのインストール

# macOS/Linux
curl -sSfL https://get.tur.so/install.sh | bash

# npm経由
npm install -g @turso/cli

# ログイン
turso auth login

データベースの作成

# データベースを作成
turso db create my-database

# ロケーションを指定して作成
turso db create my-database --location nrt

# 利用可能なロケーションを確認
turso db locations

# データベース一覧
turso db list

# データベース情報を表示
turso db show my-database

認証トークンの取得

# データベースの接続URLを取得
turso db show my-database --url

# 認証トークンを生成
turso db tokens create my-database

# 環境変数に設定
export TURSO_DATABASE_URL="libsql://your-db.turso.io"
export TURSO_AUTH_TOKEN="your-auth-token"

クライアントライブラリの使用

@libsql/clientのインストール

npm install @libsql/client

基本的な接続

import { createClient } from '@libsql/client';

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// シンプルなクエリ
const result = await client.execute('SELECT * FROM users');
console.log(result.rows);

// パラメータ付きクエリ
const user = await client.execute({
  sql: 'SELECT * FROM users WHERE id = ?',
  args: [1],
});

// 複数のクエリをバッチ実行
const results = await client.batch([
  'SELECT * FROM users',
  'SELECT * FROM posts',
  {
    sql: 'SELECT * FROM comments WHERE user_id = ?',
    args: [1],
  },
]);

トランザクション

// トランザクションを使用
const tx = await client.transaction('write');

try {
  await tx.execute({
    sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
    args: ['John Doe', 'john@example.com'],
  });

  await tx.execute({
    sql: 'INSERT INTO profiles (user_id, bio) VALUES (last_insert_rowid(), ?)',
    args: ['Software Developer'],
  });

  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
}

Drizzle ORMとの連携

セットアップ

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

スキーマ定義

// db/schema.ts
import { sqliteTable, text, integer } 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' })
    .notNull()
    .$defaultFn(() => new Date()),
});

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  userId: integer('user_id')
    .notNull()
    .references(() => users.id),
  title: text('title').notNull(),
  content: text('content').notNull(),
  published: integer('published', { mode: 'boolean' }).notNull().default(false),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
});

export const comments = sqliteTable('comments', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  postId: integer('post_id')
    .notNull()
    .references(() => posts.id),
  userId: integer('user_id')
    .notNull()
    .references(() => users.id),
  content: text('content').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
});

Drizzle設定

// 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;

データベース接続

// db/index.ts
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
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 });

マイグレーション

# マイグレーションファイルを生成
npx drizzle-kit generate:sqlite

# マイグレーションを実行
npx drizzle-kit push:sqlite

CRUD操作

// db/queries.ts
import { db } from './index';
import { users, posts, comments } from './schema';
import { eq, and, desc } from 'drizzle-orm';

// ユーザーの作成
export async function createUser(name: string, email: string) {
  const [user] = await db
    .insert(users)
    .values({ name, email })
    .returning();
  return user;
}

// ユーザーの取得
export async function getUser(id: number) {
  const user = await db.query.users.findFirst({
    where: eq(users.id, id),
  });
  return user;
}

// ユーザーの更新
export async function updateUser(id: number, data: { name?: string; email?: string }) {
  const [updated] = await db
    .update(users)
    .set(data)
    .where(eq(users.id, id))
    .returning();
  return updated;
}

// ユーザーの削除
export async function deleteUser(id: number) {
  await db.delete(users).where(eq(users.id, id));
}

// リレーションを含むクエリ
export async function getUserWithPosts(userId: number) {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
    with: {
      posts: {
        orderBy: [desc(posts.createdAt)],
      },
    },
  });
  return user;
}

// 複雑なクエリ
export async function getPublishedPostsWithComments() {
  const postsWithComments = await db.query.posts.findMany({
    where: eq(posts.published, true),
    with: {
      user: true,
      comments: {
        with: {
          user: true,
        },
        orderBy: [desc(comments.createdAt)],
      },
    },
    orderBy: [desc(posts.createdAt)],
  });
  return postsWithComments;
}

レプリケーション

マルチリージョン設定

# プライマリデータベースを作成(東京)
turso db create my-db --location nrt

# レプリカを追加(ニューヨーク)
turso db replicate my-db --location ord

# レプリカを追加(フランクフルト)
turso db replicate my-db --location fra

# レプリケーション状況を確認
turso db show my-db

読み取りレプリカの使用

// プライマリとレプリカの設定
const primaryClient = createClient({
  url: process.env.TURSO_PRIMARY_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

const replicaClient = createClient({
  url: process.env.TURSO_REPLICA_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// 書き込みはプライマリへ
export async function createPost(data: { userId: number; title: string; content: string }) {
  const [post] = await drizzle(primaryClient, { schema })
    .insert(posts)
    .values(data)
    .returning();
  return post;
}

// 読み取りはレプリカから
export async function getPosts() {
  const allPosts = await drizzle(replicaClient, { schema }).query.posts.findMany({
    orderBy: [desc(posts.createdAt)],
  });
  return allPosts;
}

エッジ最適化

// エッジファンクションでの使用例(Vercel Edge)
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from '@/db/schema';

export const runtime = 'edge';

export async function GET(request: Request) {
  const client = createClient({
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  });

  const db = drizzle(client, { schema });

  const users = await db.query.users.findMany({
    limit: 10,
  });

  return Response.json(users);
}

エンベデッドレプリカ

ローカルレプリカの使用

import { createClient } from '@libsql/client';

// エンベデッドレプリカを使用
const client = createClient({
  url: 'file:local.db',
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// 定期的に同期
setInterval(async () => {
  await client.sync();
  console.log('Synced with remote database');
}, 60000); // 1分ごと

オフライン対応

// オフライン時はローカルDBを使用
const client = createClient({
  url: 'file:local.db',
  syncUrl: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60, // 自動同期(秒)
});

// 手動同期
async function syncDatabase() {
  try {
    await client.sync();
    console.log('Database synced successfully');
  } catch (error) {
    console.error('Sync failed:', error);
    // オフライン時でもローカルデータを使用可能
  }
}

実践例

Next.js App Routerでの使用

// 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.query.users.findMany();
  return NextResponse.json(allUsers);
}

export async function POST(request: Request) {
  const body = await request.json();
  const [user] = await db.insert(users).values(body).returning();
  return NextResponse.json(user, { status: 201 });
}

Server Actionsでの使用

// app/actions/users.ts
'use server';

import { db } from '@/db';
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;

  const [user] = await db
    .insert(users)
    .values({ name, email })
    .returning();

  revalidatePath('/users');
  return user;
}

Cloudflare Workersでの使用

// worker.ts
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = createClient({
      url: env.TURSO_DATABASE_URL,
      authToken: env.TURSO_AUTH_TOKEN,
    });

    const db = drizzle(client, { schema });

    const users = await db.query.users.findMany();

    return Response.json(users);
  },
};

パフォーマンス最適化

バッチクエリ

// 複数のクエリをバッチ実行
const results = await client.batch([
  { sql: 'SELECT * FROM users WHERE id = ?', args: [1] },
  { sql: 'SELECT * FROM posts WHERE user_id = ?', args: [1] },
  { sql: 'SELECT * FROM comments WHERE user_id = ?', args: [1] },
]);

const [userResult, postsResult, commentsResult] = results;

プリペアドステートメント

// プリペアドステートメントでパフォーマンス向上
const stmt = await client.prepare('SELECT * FROM users WHERE id = ?');

const user1 = await stmt.execute([1]);
const user2 = await stmt.execute([2]);
const user3 = await stmt.execute([3]);

インデックスの活用

-- インデックスを作成
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_users_email ON users(email);

まとめ

Turso + LibSQLは、エッジコンピューティング時代のデータベースソリューションです。

  • グローバル分散: 世界中で低レイテンシーアクセス
  • SQLite互換: 既存の知識とツールを活用可能
  • エッジ最適化: サーバーレス環境に最適
  • Drizzle ORM: 型安全なクエリビルダー
  • レプリケーション: マルチリージョンでのデータ同期

エッジファンクションとの組み合わせにより、高速でスケーラブルなアプリケーションを構築できます。