🍙

実用に耐えうるCloudflare D1で使えるORM(ぽいのも含む)達

2023/05/01に公開

現状Cloudflare D1で使用できるORMとその使用方法ついてに纏めておこうという自分のメモを兼ねた記事です。記事中でRemixとの組み合わせで書いてますが、最後のSuperflare以外はRemixは特に必要ありません。

前提条件

  • ORMだけでなく、Databaseのマイグレーションも出来て運用に耐えるものを選択

今回の記事ではできるだけ実運用に耐えうるものを書いてみました。

まとめ

  • Kyselyはクエリビルダーだけあって、ORMでは届かないSQLを書き足す場合には採用の価値あり
  • D1だけで見るとDrizzleは意外と使える。(Prismaに比べると見劣るのは仕方ない)
  • Superflareはまだまだアルファの域を出ないのでProductionでは採用出来ない。

今の所、初手で選ぶなら Drizzle >= Kysely >>>>>> Superflare かなという印象です。

Kysely

https://github.com/valtyr/prisma-kysely

まずはKysely。正確に言うとこれはORMではないです。クエリビルダーです。が、後述するものとほぼ同等のことを他を組み合わすと出来ます。出来上がったサンプルのリポジトリはこちらです。

https://github.com/chimame/remix-kysely-d1

前提条件に書いてる通り、マイグレーションも必要です。Kyselyもマイグレーションの機能は持っていますが、TypeScriptの型生成に若干の工夫が必要です。そこで今回は型生成に prisma-kyselyPrismaのマイグレーションとKyselyの型生成を同時に行うということをします。(ちなみに前回の記事中では触れていませんが、リンクに貼っているリポジトリ内で使用しているマイグレーションはsqldefを使用しています。)

こちらのリポジトリのコミットを見れば手順がわかるのですが、解説していきます。

1. Prismaのセットアップ

まずは普通にPrismaをセットアップします。

$ npm install prisma --save-dev 
$ npx prisma init --datasource-provider sqlite

ただし、D1はSQLiteなのでプロバイダーはSQLiteを指定するようにします。

2. Kyselyのセットアップ

次にアプリケーションで使用するKyselyをセットアップします。

$ npm install kysely kysely-d1
$ npm install -D prisma-kysely

ここでは kysely と合わせて prisma-kyselyを入れています。これは何かというとPrismaでマイグレーションを行うことで、Kysely用の型定義を出力してくれるライブラリです。

3. データベースマイグレーション

では実際のマイグレーションをPrismaのスキーマに書き込んで行います。

prisma/schema.prisma
generator kysely {
    provider = "prisma-kysely"
    // Optionally provide a destination directory for the generated file
    // and a filename of your choice
    output = "../app/database"
    fileName = "types.ts"
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}
.env
DATABASE_URL="file:../.wrangler/state/d1/DB.sqlite3"

model部分に関してはPrisma公式のサンプルと同じものを書いただけです。重要なのは generator 部分です。Kysely用の型を app/database/types.ts というパスに出力してくれます。
上記を記載後に、Prismaの通常通りマイグレーションを行う npx prisma migrate dev を実行することで、マイグレーション(prisma/migrations)への出力と実行を行ってくれます。

もう1つ重要なのはPrismaがSQLiteを読み込む先の設定です。デフォルトの設定ではなく、後に使用するD1のローカルファイル出力先に設定しておきます。こうすることで D1のローカルでの参照先をPrismaでマイグレートするということになります。もちろんこれはローカルで開発する場合のみに使用することになります。

4. D1のセットアップ

続いてD1を使用できるようにセットアップを行います。

$ npx wrangler d1 create kysely-sample

これを実行するとこで以下のような記述を wrangler.toml に追記するような出力が現れると思います。

wrangler.toml
[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "kysely-sample"
database_id = "uuid"

とりあえずはこれを wrangler.toml に追記すればいいです。後は開発環境の実行時に --local --persist というオプションを付けて実行すればいいです。サンプルはRemixなので以下のような実行するためのnpmスクリプトが記載されています。

package.json
{
  ...
  "scripts": {
    ...
    "wrangler": "wrangler pages dev ./public --local --persist"
  },
  ...
}

5. 本番環境へのマイグレート

先程実行したPrismaマイグレートはあくまでローカルのSQLiteへのマイグレートでした。実際のD1へのマイグレートは wrangler コマンドからマイグレートする必要があります。

$ npx wrangler d1 migrations apply kysely-sample

wrangler コマンドでマイグレートする場合にはマイグレートファイルを読み込む必要があります。Prismaが作成したマイグレートファイルを wrangler コマンドで読み込めるようにするには少し工夫が必要になります。
まずは wrangler コマンドで読み込むマイグレートファイルのパスを変更できるのでします。

wrangler.toml
[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "kysely-sample"
database_id = "uuid"
migrations_dir = ".wrangler/migrations" #<= これを追加

正直場所はどこでもいいですが、私のサンプルではgitignoreに設定しているパスに出力したということだけです。読み込む先に指定した場所へPrismaのマイグレートファイルをコピーします。以下はコピーして、マイグレーションを実行するための zx シェルです。

scripts/migration_apply.mjs
const migrations = await glob(['prisma/migrations/*/migration.sql'])

await $`mkdir -p ./.wrangler/migrations`

for (let i =0; i < packages.length; i++) {
  const migrationName = packages[i].replace('prisma/migrations/', '').split('/')[0]
  await $`cp ${packages[i]} .wrangler/migrations/${migrationName}.sql`
}

await $`npx wrangler d1 migrations apply kysely-sample`

Prismaは prisma/migrations/<migration name>/migration.sql というファイルを出力してマイグレーションを行うので、それをD1用に .wrangler/migrations/<migration name>.sql という場所にコピーして実行するというスクリプトです。こうすることで 本番のD1へは migration_apply.mjs (wrangler)を使ってマイグレーションする ということが可能です。

6. アプリケーションからデータベースに接続

データベースが出来たので、最後にアプリケーションからデータベースへ接続するコードの例を以下に記載します。

app/database/client.server.ts
import type { DB } from './types'

import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'

export const client = (database: any) => new Kysely<DB>({ dialect: new D1Dialect({ database }) })
/app/routes/users.tsx
import { LoaderArgs } from "@remix-run/cloudflare"
import { client } from "~/database/client.server"

export const loader = async ({ context }: LoaderArgs) => {
  const users = await client(context.DB).selectFrom('User').selectAll().execute()

  return {
    users
  }
}

という風にデータベースからユーザデータを取得するようなコードが書けます。

Drizzle

D1を正式にサポートしているORMは少ないのですが、このDrizzleというORMは正式にサポートされています。Kyselyとは違い、これだけでD1を正式にサポートしているとあって、色々とかゆいところに手が届くし、Kyselyより導入は簡単です。

https://github.com/chimame/remix-drizzle-d1

では以下に細かい解説を書いていきます。

1. Drizzleのインストール

これはREADMEに書いている通り、パッケージをインストールします。

$ npm install drizzle-orm
$ npm install -D drizzle-kit

2. データベースマイグレーション

これは先程書いたKysely(Prisma)と比べれるようにできるだけ近い形のマイグレートを書きました。

app/db/schema.ts
export * from './schema/users'
export * from './schema/posts'
app/db/schema/users.ts
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'

export const users = sqliteTable('users', {
  id: integer('id').primaryKey(),
  email: text('email').notNull(),
  name: text('name'),
  createdAt: text('createdAt').default(sql`CURRENT_TIMESTAMP`).notNull(),
  updatedAt: text('updatedAt').default(sql`CURRENT_TIMESTAMP`).notNull(),
}, (users) => ({
  emailIdx: uniqueIndex('emailIdx').on(users.email)
}))
app/db/schema/posts.ts
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { users } from './users'

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  published: integer('published').notNull().default(0), // boolean型が作れない https://github.com/drizzle-team/drizzle-orm/pull/411
  authorId: integer('authorId').references(() => users.id),
})

注意が必要なのはSQLiteなのでBoolean型やTimestamp型というものはありません。何かで代用するしかないです。Prismaの場合はそこをよしなに吸収してはくれますが、結局Kyselyでも取り出す場合は同じようなことになります。

上記のスキーマファイルを生成後に、Drizzleの設定をファイルを以下のように設定し、マイグレーションコマンドを実行することでマイグレーションのSQLを出力してくれます。

drizzle.config.json
{
  "out": "./migrations", // どこに出力するか?wrangler d1のマイグレーション実行時の読み込みデフォルトパスに出力する
  "schema": "./app/db",
  "breakpoints": false
}
$ npx drizzle-kit generate:sqlite
$ npx wrangler d1 migrations apply drizzle-sample --local

この2つのコマンドを実行することでマイグレーションのSQL出力と、D1(ローカル)への反映を行ってくれます。本番のデータベースへ反映するには --local を外せば反映されます。

3. アプリケーションからデータベースに接続

データベースが出来たので、最後にアプリケーションからデータベースへ接続するコードの例を以下に記載します。

app/db/client.server.ts
import { drizzle } from 'drizzle-orm/d1'

export const client = (database: any) => drizzle(database)
/app/routes/users.tsx
import { LoaderArgs } from "@remix-run/cloudflare"
import { client } from "~/db/client.server"

export const loader = async ({ context }: LoaderArgs) => {
  const users = await client(context.DB).selectFrom('User').selectAll().execute()

  return {
    users
  }
}

という風にデータベースからユーザデータを取得するようなコードが書けます。

Superflare

最後にSuperflareです。こいつは上の2つとは違いフレームワークに近いものです。Cloudflareの機能にロックインしたフレームワークですが、ロックインした分Cloudflareの機能を快適に使える代物です。

ただし、以下の注意事項がありますので、使用は慎重に検討ください。

  • 現状はRemixでしか動作しません。
  • Remixプロジェクトの後からSuperflareを入れるのは結構苦労します。(RequestHandlerの修正やCloudflare Workersの型やビルドなどなど結構手が入ります)
  • 使用してみた限りAlpha版です。コンセプトとして出している状況に近いです。

https://github.com/chimame/remix-superflare-d1

それでは上記の解説に行きます。もちろん最後の参考資料にドキュメントや他方の記事もありますのでそちらも合わせてご覧ください。

1. プロジェクトの作成

Remixプロジェクトを作ってからSuperflareを入れるのはちょっとしんどいです。なのでsuperflareの初期セットアップから実施します。

$ npx supreflare@latest new

これでRemixとSuperflareが組み合わさったプロジェクトが出来上がります。その後にnpm packagesをインストールしますが、現状は以下のコマンドでしかインストール出来ません。

$ npm install --legacy-peer-deps
# or yarn install

これはRemixが使用する @cloudflare/workers-type のバージョンとSuperflareが使用する @cloudflare/workers-type のバージョンが異なるためにインストール出来ないからです。

2. データベースマイグレーション

マイグレーションはSuperflareが機能として提供してくれます。基本的な流れとしては以下です。

  1. npx superflare generate migration <migration name> でマイグレーションを作る
  2. app/db/migrations/xxxx_<migration name>.ts のファイルを修正する
  3. npx superflare migration でデータベースに反映させる

今回は他の2つ同様なテーブルになるように以下のようなマイグレーションファイルを作っています。

app/db/migrations/0001_add_posts.ts
import { Schema } from 'superflare';

export default function () {
  return Schema.create("posts", (table) => {
    table.increments("id");
    table.string("title");
    table.string("content").nullable();
    table.boolean("published");
    table.integer("userId");
    table.timestamps();
  });
}
app/db/migrations/0002_add_column_name_to_users.ts
import { Schema } from 'superflare';

export default function () {
  return Schema.update("users", (table) => {
    table.text("name").nullable();
  });
}

Posts というテーブルを新たに作る」というものと「Users テーブルに name というカラムを追加する」というものを表現しています。これを作ってからマイグレーションコマンドを実行するとデータベースに反映されれます。

$ npx superflare migration

3. アプリケーションからデータベースに接続

次にこのテーブルのデータを取得できるように、まずはモデルというものを作成します。この辺はRailsライクなフレームワークを触った人なら結構馴染みのあるものではないでしょうか。

app/models/User.ts
import { Model } from "superflare";
import { Post } from './Post';

export class User extends Model {
  toJSON(): Omit<UserRow, "password"> {
    const { password, ...rest } = super.toJSON();
    return rest;
  }
  posts!: Post[] | Promise<Post[]>;
  $posts() {
    return this.hasMany(Post);
  }
}

Model.register(User);

export interface User extends UserRow {}
app/models/Posts.ts
import { Model } from "superflare";

export class Post extends Model {
  toJSON(): PostRow {
    return super.toJSON();
  }
}

Model.register(Post);

export interface Post extends PostRow {}

最後に作ったモデルを参照してデータを取得することが以下のようなコードで可能になります。

/app/routes/users.tsx
import { LoaderArgs } from "@remix-run/cloudflare"
import { User } from "~/models/User"

export const loader = async ({ context }: LoaderArgs) => {
  const users = await User.all()

  return {
    users
  }
}

find 関数やリレーションなどの取得方法を見るとますますActiveRecordぽい感じがします。

https://superflare.dev/database/relationships

でもやっぱり触れば触るほど、Alpha版の匂いがして、これ以上は触る気にならなかった。Remixに依存してもいいけど、もうちょっと仲良く作らないとさすがに触る気が起きない。(RemixもCloudflare Workersの最新版への対応とか遅いけどさ、、、)

参考資料

https://kysely-org.github.io/kysely/

https://github.com/drizzle-team/drizzle-orm/blob/main/examples/cloudflare-d1/README.md

https://superflare.dev/

Discussion