💻

D1をやめてTursoに移行した話

2024/09/24に公開

はじめに

サイドプロジェクトでずっとD1を使用していましたが、最近色々と不便を感じたり、D1の仕様に限界を感じたため、Tursoに移行することにしました。

本記事のスコープ

本記事では、D1からTursoに移行するまでの作業をまとめたいと思います。
ただしデータの移行については割愛します。

前提条件

現在のプロジェクトでは下記の技術スタックを使用しています。

  • 言語: TypeScript
  • フレームワーク: Next.js
  • DB: D1
  • ORM: drizzle-orm
  • Runtime: Bun
  • Auth: NextAuth
  • API: tRPC

そもそもなぜ移行しようと思ったのか

主に下記の理由で移行しようと思いました。
今後D1のアップデートにより改善される可能性もありますね。

  • D1の最大サイズは10GBまでしかなく、ある程度の規模になるとマルチDB運用にしないといけない
  • Worker以外からのアクセスは遅い
  • AnalyticsはTursoの方が充実している

移行作業

1. Tursoの前準備

まずはTurso CLIをインストールします。

brew install tursodatabase/tap/turso

続いて、package.jsonにdb関連の設定を変更します。
✴︎ここでついでにwrangler.tomld1_databaseの設定も削除しておきます。

  "scripts": {
    "db:generate": "drizzle-kit generate",
-    "db:migrate:local": "wrangler d1 migrations apply <db-name> --local",
-    "db:migrate:prod": "wrangler d1 migrations apply <db-name> --remote",
-    "db:studio:local": "LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio",
-    "db:studio:prod": "source .drizzle.env && DB_ID='db-id' drizzle-kit studio",
+    "db:migrate": "drizzle-kit migrate",
+    "db:studio": "drizzle-kit studio",
    ...

続いて、Turso CLIでログインして、 database credentialsを取得します。
✴︎事前にTursoコンソールでDBを作成していました。

# ブラウザで認証する
turso auth login

# DBのURLを取得する
turso db show --url <db-name>

# output: libsql://xxx.turso.io

# DBの認証トークンを取得する
turso db tokens create <db-name>

# output: eyJhb....

続いて、DB URLと認証トークンを.envに追記します。

TURSO_DATABASE_URL=libsql://xxx.turso.io
TURSO_AUTH_TOKEN=eyJhb....

2. schemaについて

D1でもTursoでもdrizzle-orm/sqlite-coreを使用しているため、schemaはそのまま使用できます。

3. drizzle.config.tsの編集

下記のように、drizzle.config.tsを編集します。

import type { Config } from "drizzle-kit";

- const { LOCAL_DB_PATH, DB_ID, D1_TOKEN, CF_ACCOUNT_ID } = process.env;
+ const { TURSO_DATABASE_URL, TURSO_AUTH_TOKEN } = process.env;

// Use better-sqlite driver for local development
export default {
    schema: "./src/server/db/schema.ts",
    out: "./migrations",
    dialect: "sqlite",
-    driver: "d1-http",
+    driver: "turso",
    dbCredentials: {
-      databaseId: DB_ID!,
-      token: D1_TOKEN!,
-      accountId: CF_ACCOUNT_ID!,
+      url: TURSO_DATABASE_URL!,
+      authToken: TURSO_AUTH_TOKEN!,
    },
} satisfies Config;

4. connect 設定

connect関連の設定を編集します。

- import { getRequestContext } from "@cloudflare/next-on-pages";
- import { drizzle } from "drizzle-orm/d1";

- import * as schema from "./schema";

- const globalForDb = globalThis as unknown as {
-  client?: D1Database;
- };

- export let client: D1Database | undefined;

- export const db = () => {
-  client = globalForDb.client ?? getRequestContext().env.DB;
-  if (env.NODE_ENV !== "production") globalForDb.client = client;
-  return drizzle(client, { schema });
- };

+ import { drizzle } from "drizzle-orm/libsql";
+ import { createClient } from "@libsql/client";
+ import { env } from "@/env";

+ const turso = createClient({
+   url: env.TURSO_DATABASE_URL,
+   authToken: env.TURSO_AUTH_TOKEN,
+ });

+ export const db = drizzle(turso);

5. NextAuthの修正

NextAuthの修正を行います。

...
- import { drizzle } from "drizzle-orm/d1"
- import { getRequestContext } from "@cloudflare/next-on-pages"
+ import { db } from "@/server/db"

export const { handlers, auth, signIn, signOut } = NextAuth(() => {

-   const db = drizzle(getRequestContext().env.DB)

    return {
        adapter: DrizzleAdapter(db, {
            usersTable: users,
            accountsTable: accounts,
            sessionsTable: sessions,
            verificationTokensTable: verificationTokens,
        }),
        session: { strategy: "jwt" },
        providers: [GitHub, Google, Facebook, Twitter, Apple],
        callbacks: {
            session: async ({ session, token }) => {
                session.user.id = token.id as string;
                return session;
            },
            jwt({ token, user }) {
                if (user) {
                    token.id = user.id;
                }
                return token;
            },
        },
    }
})

6. tRPCの修正

tRPCの修正を行います。

...
import { db } from "@/server/db";

export const createTRPCContext = async (opts: { headers: Headers }) => {
  const session = await auth();
  return {
-     db: db(),
+     db: db,
    session,
    ratelimit: unkey(),
    ...opts,
  };
};

7. マイグレーション

マイグレーションできてから一度コンソール上で確認するか、bun run db:studioで確認します。

bun run db:generate
bun run db:migrate

---
$ drizzle-kit migrate
No config path provided, using default 'drizzle.config.ts'
Reading config file 'xxxx/drizzle.config.ts'
[] migrations applied successfully!%               

以上で全ての修正が完了しました。

困ったこと

queryの方法を変えないといけない

クライアントがdrizzle-orm/d1 -> drizzle-orm/libsqlに変わったため、以前のqueryの方法を変えないといけない。
例えば、下記のようにtursoの場合、ctx.db.query.xxxのような方法ではなく、ctx.db.select().from(xxx)のような方法でqueryする必要があります。

before:

export const userRouter = createTRPCRouter({
    getById: protectedProcedure
        .input(z.object({ id: z.string() }))
        .query(async ({ ctx, input }) => {
            const user = await ctx.db.query.users.findFirst({
                where: eq(users.id, input.id),
            });
            if (!user) throw new Error("User not found");
            return user;
        }),

        ...

after:

import { users } from "@/server/db/schema";

export const userRouter = createTRPCRouter({
    getById: protectedProcedure
        .input(z.object({ id: z.string() }))
        .query(async ({ ctx, input }) => {
            const user = await ctx.db.select().from(users).where(eq(users.id, input.id))
            if (!user) throw new Error("User not found");
            return user;
        }),
        ...

レイテンシーが期待よりは早くなっていない?

ローカルで検証したところ、レイテンシーが期待よりは早くなっていないように感じました。
大体200ms ~ 800msくらいでした。

まとめ

今回はD1からTursoに移行する手順をまとめました。
少しギャップもありましたが、移行自体はスムーズに行けてほぼ満足です。
今後D1のアップデートにより改善される可能性もあるので、期待しています。

Discussion