Cloudflare D1 + PrismaでGraphQLサーバーを立ててみる
個人開発の技術選定で、Cloudflare Workers + D1の組み合わせが気になっています。先月D1がGAになり、対応が遅れていたPrismaもD1対応したということで(Previewですが)、試してみました。
コードはこちらに載せています。
構成
ランタイム: Cloudflare Workers
ただ、いくらWebサーバーがEdgeに乗ってて早くてもDBが特定リージョンにあったら遅いよね、という悩みがあり、それを解決するべくCloudflareが提案しているのがD1です。
DB: Cloudflare D1
ぶっちゃけ、D1使ってみたい!というのが今回のモチベーションです。
ORM: Prisma ORM
TypeScript界隈でORMといえばPrismaみたいな風潮がある(ような気がする)。
D1に対応したのはいいけれど、バンドルサイズが1MB弱になってしまって辛い(Workersのサイズ上限)、みたいな話もあるけど使ってみる。
GraphQL Server: GraphQL Yoga
参考にさせていただいた記事の構成を踏襲した。
特に大きな理由はないけど、Cloudflare Workers上で動くWebサーバーとなると割と選択肢が絞られて、GraphQL Schema Builder: Pothos
同上。ただ、せっかくPrismaがD1対応したので、Prismaのプラグインを使ってみる。
Cloudflare Workers + D1 + Prismaを動かす
前提として、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"]
を書き足す。
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
Cloudflare D1をつくる
$ npx wrangler d1 create d1-prisma-sample-dev
以下のような出力がでるので、これを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モデルを定義する。
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
を修正。
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
import SchemaBuilder from '@pothos/core';
const builder = new SchemaBuilder({});
builder.queryType({
fields: (t) => ({
hello: t.string({
resolve: () => 'world',
}),
}),
});
export const schema = builder.toSchema();
まずはGraphQL化して動作確認。
// 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
に追記。
generator pothos {
provider = "prisma-pothos-types"
}
$ npx prisma generate
$ touch src/builder.ts
$ touch src/context.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の型推論が遅くなることらしいので、クリティカルな問題ではないと思います。
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
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
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();
},
}),
}));
import { builder } from './builder';
import './models/User';
import './resolvers/User';
export const schema = builder.toSchema();
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.ts
のdmmf
ですね。
これの回避方法はこちらの記事に記載がありました(Discodeは追えてないので、解決方法の提供者は分からず)。
$ npm install https://github.com/repository/prisma-dmmf-generator
以下を追記
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
の該当部分を修正。
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
まとめ
できたもの
- Cloudflare D1 + PrismaのGraphQLサーバーができた
- Prismaのバンドルサイズ云々は、無料プランで普通にデプロイできた
- PrismaのD1連携はまだPreviewなので要ウォッチ
- この先バンドルサイズが問題になるなら、Drizzleなどにシフトしたほうがいいかも
- ContextにいれたPrismaClientは、今後の開発で問題になる可能性がある
- dmmfの部分はWorkaround的なものだと思うので、状況が変わる可能性がある
- この先上記2点が問題になるなら、
@pothos/plugin-prisma
を@pothos/plugin-simple-objects
に切り替える
参考
Discussion