概要
Prismaを触ってみて、ApolloでGraphQLサーバー建てるところまでやってみたのでその備忘録。
参考にしたチュートリアル
https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-typescript-postgres/
この記事のコードのリポジトリ
https://github.com/yuuyu00/prisma-apollo
プロジェクト作成
サクッと雛形を作る。
とりあえずPrismaの依存だけインストールする。
mkdir prisma-apollo && cd prisma-apollo
yarn init -y
yarn add @prisma/cli typescript ts-node @types/node
npx prisma
で、インストールした @prisma/cli
を実行することができる。
tsconfig.json
以下のtsconfig.jsonを追加する。
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
Prismaのセットアップ
prisma init
npx prisma init
を実行して、Prismaのスキーマやマイグレーションファイルを生成する。
生成されたファイルは prisma/
以下にある。
DB設定
今回はpostgresを使用するので、使っていなければ先に起動できる状態までセットアップしておく。
postgresの使用するユーザー名、パスワード、ホスト、DB名がわかれば、 prisma init
によって生成された.envにDB接続情報を書く。
# DATABASE_URLは以下の書式に従う。
# postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
# 例えばDBユーザー名がtarou, パスワードがhoge, ホスト名がlocalhost, ポート番号が5432, DB名がmydb, スキーマ名がpublic(デフォルトではpublic)の場合、
DATABASE_URL="postgresql://tarou:hoge@localhost:5432/mydb?schema=public"
# のように記述する。
# macOSにpostgresをインストールすると、ログインユーザーがデフォルトのユーザーとして設定される。
# その場合、ユーザー名とパスワードがどちらもMacのログインユーザー名になる。
Prismaのスキーマを編集する
prisma/prisma.schema
を開き、以下の内容を追加する。
model Post {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Profile {
id Int @default(autoincrement()) @id
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
posts Post[]
profile Profile?
}
モデルの記述については以下を参照(読まなくてもまあなんとなくわかると思う)
https://www.prisma.io/docs/concepts/components/prisma-schema/data-model
マイグレーションファイルを生成&DBに適用する
以下のコマンドを実行する。
npx prisma migrate dev --name init --preview-feature
# dev -> マイグレーションファイルを生成して、すぐにDBに変更を適用する
# --name -> マイグレーションファイルの末尾につける名前を指定する
# --preview-feature -> プレビュー版の機能を使う。業務のプロジェクトで使うなよ!
これを実行すると、prisma.schemaからマイグレーションファイルが生成され、その内容がただちにDBに適用される。
prisma clientを使う
@prisma/client
をインストールして、PrismaClientを使ってDBにアクセスする。
使い方はこんな感じ:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// userモデルのすべてのレコードを取得する
const getUsers = async () => {
const users = await prisma.user.findMany();
return users;
}
// userモデルへのリレーションを持つauthorプロパティを含めたpostモデルのレコードを、idを指定して1件取得する
const getPost = async (postId: number) => {
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { author: true },
});
return post
}
// userモデルへのリレーションを持つauthorプロパティを含めたpostモデルのレコードを、1件作成する
const createPost = async (
title: string,
published: boolean,
authorId: number,
content?: string
) => {
context.prisma.post.create({
data: {
title,
content,
published,
// connectプロパティによってUserテーブルの既存のレコードと結合する
author: { connect: { id: authorId } },
},
});
};
GraphQLサーバーの実装
ライブラリインストール
スクリプトとか含めて全部書くの大変なので概要に書いたリポジトリのpackage.json参照
スクリプトも同じように書いておく。
GraphQLスキーマを書く
PrismaのモデルをGraphQLスキーマのtypeに移し、queryやmutationを書く。
type Post {
id: Int!
createdAt: String!
updatedAt: String!
title: String!
content: String
published: Boolean!
author: User!
}
type Profile {
id: Int!
bio: String
user: User!
}
type User {
id: Int!
email: String!
name: String
posts: [Post!]!
profile: Profile
}
input CreateUserInput {
email: String!
name: String!
}
input CreatePostInput {
title: String!
content: String
published: Boolean!
authorId: Int!
}
type Mutation {
createUser(input: CreateUserInput): User!
createPost(input: CreatePostInput): Post!
}
type Query {
user(id: Int!): User
posts: [Post!]!
}
authorIdやuserIdなどは冗長なので、モデルのtypeからは除外した。
Apolloサーバー追加
srcディレクトリを追加し、server.tsに以下のようにApolloServerを建てるためのコードを書く。
まだほとんどのファイルを実装してないのでエラーになるが無視
import { ApolloServer, gql, IResolvers } from "apollo-server";
import { Resolvers } from "../gqlTypes";
import { readFileSync } from "fs";
import { createContext } from "./context";
import { resolvers } from "./resolvers";
const getTypeDefs = () => {
const schemaStr = readFileSync("schema.gql", "utf8");
return gql`
${schemaStr}
`;
};
new ApolloServer({
typeDefs: getTypeDefs(),
context: createContext,
resolvers: resolvers as IResolvers<any, any> & Resolvers,
}).listen({ port: 9000 }, () => console.log("listening on 9000"));
context追加
resolverに必ず渡ってくる値であるcontextを追加する。
prismaのようなデータソースへのアクセスをcontextを介して行う。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export interface Context {
prisma: PrismaClient;
}
export const createContext: () => Context = () => {
return { prisma };
};
graphql-code-generatorでGraphQLスキーマからTSの型を生成する
プロジェクトルートにcodegen.ymlを追加し、以下のように書く。
schema: "./schema.gql"
generates:
gqlTypes.ts:
plugins:
- typescript
- typescript-resolvers
config:
mapperTypeSuffix: Model
mappers:
Post: "@prisma/client/index.d#Post"
Profile: "@prisma/client/index.d#Profile"
User: "@prisma/client/index.d#User"
contextType: ./src/context#Context
書いたら、 graphql-codegen --config codegen.yml
でTSの型を生成する。
(package.jsonのscriptsを概要のリポジトリと同じように書いていれば yarn generate
でも可)
mappersって何?
codegen.ymlのconfig -> mappersは、
「この名前に該当するGraphQLスキーマのtypeについては、スキーマからTSへの変換を行わず指定した型を使うこととする」
という設定。
なぜ使うのか
GraphQLのtypeとモデルの型のミスマッチを解消する作業をトリビアルリゾルバに任せるために設定する。
もしそうしない場合、そのミスマッチをqueryやmutationの戻り値ごとに解消しなければならず、とても面倒なことになる。
例えばmappersを設定しなかった場合、このようなエラーが出るとする
GraphQLの型にはauthorというUser型のプロパティがあるはずだが、それがないというエラーになる。
find時にauthorのリレーションまで含めて取っていないので当然だが、含めて取ったとしてもprismaからの戻り値がUser型ではなくなってしまうため、エラーは解消できない
型アサーションで無理やり通せば解決できるかもしれないが、型安全性が損なわれるし毎回そんなことをするのは治安が悪すぎる。
しかも、この方法だとauthorプロパティにアクセスしない時でも常にauthorを取得するので効率が悪いうえ、authorから先のリレーションは取っていないのでアクセスできない。それを解決しようとincludeを増やせばさらに無駄なデータ取得を増やすことになる。
これを解決するには、queryやmutationの単位では一旦prismaの型の値を返して、トリビアルリゾルバによってそれ以降のミスマッチを解決すればよい。
そのために、TSの型としてのUserはprismaが生成したUserの型を使うこととする、という設定をmappersで行う。
トリビアルリゾルバでauthorプロパティを解決する
includeオプションを付けずにPostのレコードを取得した場合、authorプロパティは含まれないのでそれを解決する必要がある。
import { PostResolvers } from "../../../gqlTypes";
export const Post: PostResolvers = {
// Post型のauthorプロパティはこの関数の実行結果を返す
// parentは、返却されようとしたこのリゾルバを通る前のPost型の値
author: async (parent, _, context) => {
const author = await context.prisma.user.findUnique({
where: { id: parent.authorId },
});
if (!author) throw new Error("author not found");
return author;
},
};
このリゾルバをQueryやMutationとともにApolloServerに渡せば、authorプロパティにアクセスした際にこのリゾルバが実行され、値が解決される。
Queryを書く
src/resolvers/queriesとディレクトリを切ってQueryを実装する。
import { QueryResolvers } from "../../../gqlTypes";
export const posts: QueryResolvers["posts"] = ({}, _args, context) =>
context.prisma.post.findMany({ include: { author: true } });
他も同様なのであとはリポジトリ参照
Mutationを書く
Queryと同様にディレクトリ切ってMutationを実装する。
import { prismaVersion } from "@prisma/client";
import { MutationResolvers } from "../../../gqlTypes";
export const createPost: MutationResolvers["createPost"] = (
{},
{ input: { title, content, published, authorId } },
context
) =>
context.prisma.post.create({
data: { title, content, published, author: { connect: { id: authorId } } },
});
他も同様なのであとはリポジトリ参照
トリビアルリゾルバを書く
前述したように、QueryやMutationの戻り値では解決されていないプロパティを解決するためのリゾルバを書く。
import { PostResolvers } from "../../../gqlTypes";
export const Post: PostResolvers = {
author: async (parent, _, context) => {
const author = await context.prisma.user.findUnique({
where: { id: parent.authorId },
});
if (!author) throw new Error("author not found");
return author;
},
};
他も同様なのであとはリポジトリ参照
リゾルバをまとめる
resolvers/index.tsにリゾルバをまとめてexportする。
import { Resolvers } from "../../gqlTypes";
import { user, posts } from "./queries";
import { createUser, createPost } from "./mutations";
import { Post, Profile, User } from "./trivials";
const Query: Resolvers["Query"] = {
user,
posts,
};
const Mutation: Resolvers["Mutation"] = {
createUser,
createPost,
};
export const resolvers: Resolvers = {
Query,
Mutation,
Post,
Profile,
User,
};
動作確認
yarn dev
でサーバーを起動して、localhost:9000にアクセスして正常に動いてることを確認したら完了。