Open4

Cloudflare Pages + Remix(v2.8) + Drizzle ORM + D1 お試し

kazuphkazuph

初手 Cloudflare + Remixの雛形の作成

npm create cloudflare@latest my-remix-app -- --framework=remix
kazuphkazuph

cf-remix-drizzle-d1

schema定義

まずなにはともあれschemaファイルを定義します。これはsqlite系のDBであれば共通です。

app/db/schema.ts
import { sql } from "drizzle-orm";
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').unique(),
  name: text('name').notNull(),
  customerId: text('customerId').unique(),
  createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
  updatedAt: text("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});

migrationファイルの生成

これを元にmigrationファイルを作成します。

npm add -D drizzle-kit drizzle-orm

drizzle-kitに食わせるconfigファイルを置きます。プロジェクトルートです。outの場所にmigraitonファイルが生成されます。

drizzle.config.ts
export default {
	dialect: "sqlite",
	schema: "./db/schema.ts",
	out: "./db/migrations",
};

ということで、schemaファイルからmigrationファイルを生成しましょう。

npx drizzle-kit generate

このようなファイルを生成されます。

sqlの中身はこんな感じになっておりschemaファイルと対応が取れていることがわかります。

db/migrations/0000_high_red_hulk.sql
CREATE TABLE `users` (
	`id` text PRIMARY KEY NOT NULL,
	`email` text,
	`name` text NOT NULL,
	`customerId` text,
	`created_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL
        `updated_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_customerId_unique` ON `users` (`customerId`);

ここで元のschemaファイルのusersにニックネームを追加してみましょう。

schema.ts
export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').unique(),
  name: text('name').notNull(),
+  nickname: text('nickname'),
  customerId: text('customerId').unique(),
  createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
  updatedAt: text("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});

この状態でもう一度generateしてみましょう。

$ npx drizzle-kit generate

drizzle-kit: v0.20.14
drizzle-orm: v0.29.4

No config path provided, using default 'drizzle.config.ts'
Reading config file '/home/kazuph/src/github.com/kazuph/cf-samples/cf-remix-drizzle-d1/drizzle.config.ts'
1 tables
users 6 columns 2 indexes 0 fks

[] Your SQL migration file ➜ db/migrations/0001_wild_magik.sql 🚀

新たにmigrationファイルが追加されたのがわかります。中身を見ると

ALTER TABLE users ADD `nickname` text;

となってます。

ここまででmigrationファイルを生成する流れがわかりました。

migrrateの実行

現時点ではDBの実体がないのでまずD1のDBを作成します。

npx wrangler d1 create mydb
...
[[ d1_databases ]]
binding = "DB"
database_name = "mydb"
database_id = "<YOUR_D1_ID>"

この出力を wranger.tomlに追記してください。またすでに作成している migrations_dir も設定しています。

wranger.toml
name = "cf-remix-drizzle-d1"
compatibility_date = "2024-03-04"

[[ d1_databases ]]
binding = "DB"
database_name = "mydb"
database_id = "<YOUR_D1_ID>"
migrations_dir = "db/migrations"

それではmigrateしてみます。

$ npx wrangler d1 migrations apply mydb --local

 ⛅️ wrangler 3.31.0
-------------------
Migrations to be applied:
┌────────────────────────┐
│ name                   │
├────────────────────────┤
│ 0000_high_red_hulk.sql │
├────────────────────────┤
│ 0001_wild_magik.sql    │
└────────────────────────┘
✔ About to apply 2 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database mydb () from .wrangler/state/v3/d1:
┌────────────────────────┬────────┐
│ name                   │ status │
├────────────────────────┼────────┤
│ 0000_high_red_hulk.sql │ ✅       │
├────────────────────────┼────────┤
│ 0001_wild_magik.sql    │ 🕒️     │
└────────────────────────┴────────┘
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database mydb () from .wrangler/state/v3/d1:
┌────────────────────────┬────────┐
│ name                   │ status │
├────────────────────────┼────────┤
│ 0000_high_red_hulk.sql │ ✅       │
├────────────────────────┼────────┤
│ 0001_wild_magik.sql    │ ✅       │
└────────────────────────┴────────┘

マイグレーションは d1_migrations テーブルで管理しているみたいなので、そのtableがあるか確認してみましょう。

$ npx wrangler d1 execute mydb --command="select * from d1_migrations;"

 ⛅️ wrangler 3.31.0
-------------------
🌀 Mapping SQL input into an array of statements
🌀 Parsing 1 statements
🌀 Executing on remote database mydb ():
🌀 To execute on your local development database, pass the --local flag to 'wrangler d1 execute'
🚣 Executed 1 commands in 0.3032ms
┌────┬─────────────────────────────┬─────────────────────┐
│ id │ name                        │ applied_at          │
├────┼─────────────────────────────┼─────────────────────┤
│ 1  │ 0000_conscious_ironclad.sql │ 2024-03-02 19:29:23 │
└────┴─────────────────────────────┴─────────────────────┘

はい、これでmigrateするところまで確認できました。

テストデータを追加する

wranglerを使ってusersを追加してみます。

npx wrangler d1 execute mydb --command="INSERT INTO users (id, email, name, nickname, customerId, created_at, updated_at) VALUES 
('1', 'user@example.com', 'ユーザー太郎', 'taro', 'cust_1234', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);" --local

確認してみましょう。

$ npx wrangler d1 execute mydb --command="select * from users;" --local                                                  
 ⛅️ wrangler 3.31.0
-------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database mydb () from .wrangler/state/v3/d1:
┌────┬──────────────────┬────────┬────────────┬─────────────────────┬──────────┬─────────────────────┐
│ id │ email            │ name   │ customerId │ created_at          │ nickname │ updated_at          │
├────┼──────────────────┼────────┼────────────┼─────────────────────┼──────────┼─────────────────────┤
│ 1  │ user@example.com │ ユーザー太郎 │ cust_1234  │ 2024-03-06 08:23:36 │ taro     │ 2024-03-06 08:23:36 │
└────┴──────────────────┴────────┴────────────┴─────────────────────┴──────────┴─────────────────────┘

はい、これで参照できるデータが作成できました。

Remixでloadする

ついにRemix上で扱えるようになります。
今回はそのまま _index.ts に書いちゃいましょう。

その前に wranger.toml から型定義を生成します。これはすでにremixの雛形生成時にコマンドを追加してくれています。

$ npm run typegen

> typegen
> wrangler types

 ⛅️ wrangler 3.31.0
-------------------
interface Env {
        DB: D1Database;
}

これを実行すると worker-configuration.d.ts が自動で生成されていてDBが参照できるようになります。

それでは、loaderを追加します。

app/_index.ts
+ import type { LoaderFunctionArgs, LoaderFunction } from "@remix-run/cloudflare";
+ import { useLoaderData } from "@remix-run/react";

+ import { drizzle } from 'drizzle-orm/d1';
+ import { users } from '../db/schema';

...

+ export const loader: LoaderFunction = async ({ context }: LoaderFunctionArgs) => {
+   const db = drizzle(context.cloudflare.env.DB);
+   const allUsers = await db.select().from(users).all();
+ 
+   return {
+     allUsers
+   };
+ };

...

export default function Index() {
+  const { allUsers } = useLoaderData<typeof loader>();

return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1>Welcome to Remix (with Vite and Cloudflare)</h1>
      <ul>
+         {allUsers.map((user) => (
+           <li key={user.id}>{user.name}</li>
+         ))}
        <li>
...

追記が終わったらさっそく実行してみます。

npm run dev

先ほど追加したユーザー太郎が表示されていれば成功です。

kazuphkazuph

本番リリース

まず本番のmydbにmigrationを反映します。

package.json
+    "db:migrate": "npx wrangler d1 migrations apply mydb --local",
+    "db:migrate:prod": "npx wrangler d1 migrations apply mydb"

違いは --local と付いているかです。

実行方法は以下です。

npm run db:migrate:prod

これで本番にもlocalと同じテーブルが作成されました。

テストデータも追加しましょう。

npx wrangler d1 execute mydb --command="INSERT INTO users (id, email, name, nickname, customerId, created_at, updated_at) VALUES 
('1', 'user@example.com', 'ユーザー太郎', 'taro', 'cust_1234', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);"

あとはpagesにdeployするだけです。これはすでにコマンドが設定されています。

npm run deploy

デプロイ完了後に数分待つとサイトが表示されるのですが、PagesあるあるでDBや環境変数等が自動では設定できないので、手動でぽちぽちします。



この状態で確かですが待ってもだめで、もう一度deployする必要があります。

npm run deploy

今度はデプロイ完了メッセージのあとに早ければ数秒でサイトが表示されます。

以上です。

kazuphkazuph

DB確認コマンド

PRAGMA table_list

$ npx wrangler d1 execute mydb --command="PRAGMA table_list"

🚣 Executed 1 commands in 0.2478ms
┌────────┬────────────────────┬───────┬──────┬────┬────────┐
│ schema │ name               │ type  │ ncol │ wr │ strict │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ main   │ users              │ table │ 7    │ 0  │ 0      │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ main   │ sqlite_sequence    │ table │ 2    │ 0  │ 0      │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ main   │ d1_migrations      │ table │ 3    │ 0  │ 0      │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ main   │ _cf_KV             │ table │ 2    │ 1  │ 0      │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ main   │ sqlite_schema      │ table │ 5    │ 0  │ 0      │
├────────┼────────────────────┼───────┼──────┼────┼────────┤
│ temp   │ sqlite_temp_schema │ table │ 5    │ 0  │ 0      │
└────────┴────────────────────┴───────┴──────┴────┴────────┘

PRAGMA table_info(TABLE_NAME)

$ npx wrangler d1 execute mydb --command="PRAGMA table_info(users)"
🚣 Executed 1 commands in 0.1754ms
┌─────┬────────────┬──────┬─────────┬───────────────────┬────┐
│ cid │ name       │ type │ notnull │ dflt_value        │ pk │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 0   │ id         │ TEXT │ 1       │                   │ 1  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 1   │ email      │ TEXT │ 0       │                   │ 0  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 2   │ name       │ TEXT │ 1       │                   │ 0  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 3   │ customerId │ TEXT │ 0       │                   │ 0  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 4   │ created_at │ TEXT │ 0       │ CURRENT_TIMESTAMP │ 0  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 5   │ nickname   │ TEXT │ 0       │                   │ 0  │
├─────┼────────────┼──────┼─────────┼───────────────────┼────┤
│ 6   │ updated_at │ TEXT │ 1       │ CURRENT_TIMESTAMP │ 0  │
└─────┴────────────┴──────┴─────────┴───────────────────┴────┘