🪶

Cloudflare D1 + PrismaでGraphQLサーバーを立ててみる

2024/05/17に公開

個人開発の技術選定で、Cloudflare Workers + D1の組み合わせが気になっています。先月D1がGAになり、対応が遅れていたPrismaもD1対応したということで(Previewですが)、試してみました。

コードはこちらに載せています。
https://github.com/mi-373/d1-prisma-sample

構成

ランタイム: Cloudflare Workers

https://developers.cloudflare.com/workers/
CloudflareのEdgeでサーバーレスができる。速そう(小並感)
ただ、いくらWebサーバーがEdgeに乗ってて早くてもDBが特定リージョンにあったら遅いよね、という悩みがあり、それを解決するべくCloudflareが提案しているのがD1です。

DB: Cloudflare D1

https://developers.cloudflare.com/d1/
Edgeで動くSQLiteです。速そう(小並感)
ぶっちゃけ、D1使ってみたい!というのが今回のモチベーションです。

ORM: Prisma ORM

https://www.prisma.io/

TypeScript界隈でORMといえばPrismaみたいな風潮がある(ような気がする)。
D1に対応したのはいいけれど、バンドルサイズが1MB弱になってしまって辛い(Workersのサイズ上限)、みたいな話もあるけど使ってみる。
https://www.prisma.io/blog/build-applications-at-the-edge-with-prisma-orm-and-cloudflare-d1-preview

GraphQL Server: GraphQL Yoga

https://the-guild.dev/graphql/yoga-server
特に大きな理由はないけど、Cloudflare Workers上で動くWebサーバーとなると割と選択肢が絞られて、参考にさせていただいた記事の構成を踏襲した。

GraphQL Schema Builder: Pothos

https://pothos-graphql.dev/
同上。ただ、せっかくPrismaがD1対応したので、Prismaのプラグインを使ってみる。

Cloudflare Workers + D1 + Prismaを動かす

https://developers.cloudflare.com/d1/tutorials/d1-and-prisma-orm/#3-create-your-d1-database
基本的にこの手順でやっていくだけなので、ざっくり書きます。
前提として、Node.js, npm, Cloudflareのアカウントが必要です。

Cloudflare Workersをつくる

$ npm create cloudflare@latest

"Hello World" WorkerとTypeScriptだけ選んで、あとはyes連打で一瞬でデプロイしてくれます。1分もかからないくらい。

Prisma ORMをいれる

$ cd d1-prisma-sample
$ npm install prisma --save-dev
$ npm install @prisma/client @prisma/adapter-d1
$ npx prisma init --datasource-provider sqlite

prisma/schema.prismaが生成されるので previewFeatures = ["driverAdapters"] を書き足す。

prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"]
}

Cloudflare D1をつくる

$ npx wrangler d1 create d1-prisma-sample-dev

以下のような出力がでるので、これをwrangler.tomlに書く。

wrangler.toml
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "d1-prisma-sample-dev"
database_id = "xxxxxxxxxxxxxxxxxxxxxxxx"

DB migration

まずは空のSQLファイルを作る。

$ npx wrangler d1 migrations create d1-prisma-sample-dev create_user_table

Userモデルを定義する。

prisma/schema.prisma
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

以下コマンドで、さっきのSQLファイルにマイグレーションコードを生成する。実行後、migrations/0001_create_user_table.sqlにSQL文が入っていることを確認。

$ npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script > migrations/0001_create_user_table.sql

DBに適用。

// For local
$ npx wrangler d1 migrations apply d1-prisma-sample-dev --local

// For remote
$ npx wrangler d1 migrations apply d1-prisma-sample-dev --remote

データを入れる。

// For local
$ npx wrangler d1 execute d1-prisma-sample-dev --command "INSERT INTO  \"User\" (\"email\", \"name\") VALUES
('jane@prisma.io', 'Jane Doe (Local)');" --local

// For remote
$ npx wrangler d1 execute d1-prisma-sample-dev --command "INSERT INTO  \"User\" (\"email\", \"name\") VALUES
('jane@prisma.io', 'Jane Doe (Remote)');" --remote

Cloudflare WorkersからD1のデータを取得する

src/index.tsを修正。

src/index.ts
import { PrismaClient } from '@prisma/client';
import { PrismaD1 } from '@prisma/adapter-d1';

export interface Env {
	DB: D1Database;
}

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		const adapter = new PrismaD1(env.DB);
		const prisma = new PrismaClient({ adapter });

		const users = await prisma.user.findMany();
		const result = JSON.stringify(users);
		return new Response(result);
	},
} satisfies ExportedHandler<Env>;
$ npx prisma generate
$ npm run dev

ローカル(localhost:8787)で動作確認できたらデプロイする。

$ npm run deploy

GraphQLサーバーをたてる

いったんGraphQLサーバーを立ててから、Prismaとの連携をやっていく。

GraphQL Yogaをいれる

スキーマ定義用のPothosも一緒に入れちゃいます。

$ npm install graphql graphql-yoga @pothos/core
$ touch src/schema.ts
src/schema.ts
import SchemaBuilder from '@pothos/core';

const builder = new SchemaBuilder({});

builder.queryType({
	fields: (t) => ({
		hello: t.string({
			resolve: () => 'world',
		}),
	}),
});

export const schema = builder.toSchema();

まずはGraphQL化して動作確認。

src/index.ts
// import { PrismaClient } from '@prisma/client';
// import { PrismaD1 } from '@prisma/adapter-d1';
import { createYoga } from 'graphql-yoga'
import { schema } from './schema'

// export interface Env {
// 	DB: D1Database;
// }

const yoga = createYoga({ schema })

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		// const adapter = new PrismaD1(env.DB);
		// const prisma = new PrismaClient({ adapter });

		// const users = await prisma.user.findMany();
		// const result = JSON.stringify(users);
		// return new Response(result);
		return yoga(request, ctx)
	},
} satisfies ExportedHandler<Env>;
$ npm run dev

localhost:8787/graphqlでプレイグラウンドが出ればOK。

GraphQL Schemaを定義する

スキーマ定義はPrismaから引っ張りたいので、Prismaプラグインを入れます。

$ npm install @pothos/plugin-prisma

以下をprisma/schema.prismaに追記。

prisma/schema.prisma
generator pothos {
  provider = "prisma-pothos-types"
}
$ npx prisma generate
$ touch src/builder.ts
$ touch src/context.ts
src/builder.ts
import SchemaBuilder from '@pothos/core';
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import type { Context } from './context';
// import dmmf from './generated/dmmf.json';

export const builder = new SchemaBuilder<{
	PrismaTypes: PrismaTypes;
	Context: Context;
}>({
	plugins: [PrismaPlugin],
	prisma: {
		client: (ctx) => ctx.db,
		// dmmf: dmmf,
		dmmf: Prisma.dmmf,
	},
});

builder.queryType();

dmmfの行はエラーが出ますが後で直します。

ctxからPrismaClientを取得してclientに渡しています。Pothos的には非推奨らしいのですが、EdgeだとPrismaClientはリクエストごとにインスタンス化する必要があるので、Contextに入れて取り回す以外にどうすればいいか分かりませんでした。ただ、非推奨の理由はVSCodeの型推論が遅くなることらしいので、クリティカルな問題ではないと思います。

context.ts
import { PrismaClient } from '@prisma/client';
import { PrismaD1 } from '@prisma/adapter-d1';

const prisma = (db: D1Database) => {
	const adapter = new PrismaD1(db);
	return new PrismaClient({ adapter });
};

export type Context = {
	db: PrismaClient;
};

export const createContext = (db: D1Database): Context => {
	return {
		db: prisma(db),
	};
};

準備ができたので、Schemaを定義します。

$ mkdir src/models
$ touch src/models/User.ts
src/models/User.ts
import { builder } from '../builder';

builder.prismaObject('User', {
	fields: (t) => ({
		id: t.exposeID('id'),
		email: t.exposeString('email'),
		name: t.exposeString('name', { nullable: true }),
	}),
});
$ mkdir src/resolvers
$ touch src/resolvers/User.ts
src/resolvers/User.ts
import { builder } from '../builder';

builder.queryFields((t) => ({
	user: t.prismaField({
		type: 'User',
		nullable: true,
		resolve: async (query, root, args, ctx, info) => {
			return await ctx.db.user.findFirst();
		},
	}),
}));
src/schema.ts
import { builder } from './builder';

import './models/User';
import './resolvers/User';

export const schema = builder.toSchema();
src/index.ts
import { createYoga } from 'graphql-yoga';
import { schema } from './schema';
import { createContext } from './context';

export interface Env {
	DB: D1Database;
}

const yoga = createYoga({ schema });

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		const context = createContext(env.DB);
		return yoga(request, context);
	},
} satisfies ExportedHandler<Env>;

dmmfをなおす

一応上記でいいはずなんですが、"[ERROR] service core:user:d1-prisma-sample: Uncaught Error: Prisma.dmmf is not available when running in edge runtimes."と怒られてしまいます。さっき後回しにしたbuilder.tsdmmfですね。

これの回避方法はこちらの記事に記載がありました(Discodeは追えてないので、解決方法の提供者は分からず)。

$ npm install https://github.com/repository/prisma-dmmf-generator

以下を追記

prisma/schema.prisma
generator dmmf {
  provider = "prisma-dmmf-generator"
  output   = "./generated_dmmf.json"
}
$ npx prisma generate
$ mkdir src/generated
$ mv prisma/generated_dmmf.json src/generated/dmmf.json

builder.tsの該当部分を修正。

src/builder.ts
import dmmf from './generated/dmmf.json';

export const builder = new SchemaBuilder<{
	PrismaTypes: PrismaTypes;
	Context: Context;
}>({
	plugins: [PrismaPlugin],
	prisma: {
		client: (ctx) => ctx.db,
		dmmf: dmmf,
	},
});

builder.queryType();

動作確認して

$ npm run dev


デプロイ。お疲れ様でした。

$ npm run deploy

まとめ

できたもの
https://github.com/mi-373/d1-prisma-sample

  • Cloudflare D1 + PrismaのGraphQLサーバーができた
  • Prismaのバンドルサイズ云々は、無料プランで普通にデプロイできた
  • PrismaのD1連携はまだPreviewなので要ウォッチ
  • この先バンドルサイズが問題になるなら、Drizzleなどにシフトしたほうがいいかも
  • ContextにいれたPrismaClientは、今後の開発で問題になる可能性がある
  • dmmfの部分はWorkaround的なものだと思うので、状況が変わる可能性がある
  • この先上記2点が問題になるなら、@pothos/plugin-prisma@pothos/plugin-simple-objectsに切り替える

参考

https://zenn.dev/chimame/articles/3e7f0f0f7e783d
https://zenn.dev/kyoizmy/scraps/e72db4ad05aa01
https://blog.cloudflare.com/prisma-orm-and-d1
https://www.prisma.io/blog/e2e-type-safety-graphql-react-3-fbV2ZVIGWg

Discussion