📖

prismaだけハンズオンのサイト

2025/03/08に公開

はじめに

prismaだけハンズオンで学習できるものを探してたら下記のものがあった
https://zenn.dev/kanasugi/articles/7f90078574bb69

何かの講座の中の一部分がprismaになってるものは、他にもある。
個人的にprismaのところがわかりにくいと思ってましたので、

prismaの部分だけある程度まとまった教材を使って、
prismaの部分だけ「ある程度のまとまった分量」をハンズオンで手を動かしていきながら、
雰囲気をつかんでおきたかった

少なすぎず、多すぎず、手ごろな分量で、これができる教材を探していたら
上記のものが見つかり、助かりました。作者には感謝。

youtube動画と、レッスン4まで問題演習と解答がついてる
動画では、おおかた解説してるが、すべては解説してない
ただ、問題演習には全て解答がついてるので自学自習できる。
詳しく深堀りしていく前に、上記のサイトにある分だけ、先に身につけた方が理解が早そうだ。

レッスン3のとき

レッスン3のときに、REST Clientについて
https://zenn.dev/tazzae999jp/articles/028dc760c856fb
の記事を書きました。

補足「npm run dev」のデバッグ起動の件
も書いたので、デバッガー使いたい人はご参考に。

レッスン4での「graphql playground」に既知のバグがありますので、その回避策を書いておきます

レッスン4のGraph QLを使うところで環境問題が一か所あった
このコンテンツが作られた当初は問題がなかったのだろう
graphql playgroundというものについて、
2024年のある時点以降の新しめのブラウザで動作させたときに、
入力支援の候補が消えずに残ってしまい
入力操作を続行するのが困難になってしまう既知のバグがある

「graphql playground」を使わない別の方法にする必要あり
試行錯誤の末、package.jsonを以下のように変更したら
レッスン4について、問題なくハンズオン形式で学習できた。

上の図の左側が元のpackage.json、右側が変更後のpackage.json

変更後のpackage.jsonをテキストで下記に示す
これをコピペしてpackage.jsonの中身を置き換えた後に、npm installして環境を作る。

{
  "name": "graphql-api",
  "license": "MIT",
  "scripts": {
    "dev": "ts-node src/index.ts"
  },
  "dependencies": {
    "@apollo/server": "^4.11.3",
    "@prisma/client": "3.15.0",
    "express": "^4.18.2",
    "graphql": "^16.6.0",
    "graphql-http": "^1.22.4",
    "graphql-scalars": "1.17.0"
  },
  "devDependencies": {
    "@types/graphql": "14.5.0",
    "@types/node": "^17.0.41",
    "prisma": "3.15.0",
    "ts-node": "10.8.1",
    "typescript": "4.7.3"
  },
  "engines": {
    "node": ">=10.0.0"
  },
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}

それから、上記のpackage.jsonの変更をうけて、
index.tsを下記のように変更する必要がある
そうしないと、npm run devでエラーになる。


import { startStandaloneServer } from "@apollo/server/standalone"; // ★ 追加

//変更前 import { ApolloServer } from "apollo-server";
import { ApolloServer } from "@apollo/server";

import { DateTimeResolver } from "graphql-scalars";
import { Context, context } from "./context";

//変更前 import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";

const typeDefs = `
type Query {
  allUsers: [User!]!
  postById(id: Int!): Post
  feed(searchString: String, skip: Int, take: Int): [Post!]!
  draftsByUser(id: Int!): [Post]
}

type Mutation {
  signupUser(name: String, email: String!): User!
  createDraft(title: String!, content: String, authorEmail: String): Post
  incrementPostViewCount(id: Int!): Post
  deletePost(id: Int!): Post
}

type User {
  id: Int!
  email: String!
  name: String
  posts: [Post!]!
}

type Post {
  id: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  content: String
  published: Boolean!
  viewCount: Int!
  author: User
}

scalar DateTime
`;

const resolvers = {
  Query: {
    allUsers: (_parent, _args, context: Context) => {
      // TODO
    },
    postById: (_parent, args: { id: number }, context: Context) => {
      // TODO
    },
    feed: (
      _parent,
      args: {
        searchString: string | undefined;
        skip: number | undefined;
        take: number | undefined;
      },
      context: Context
    ) => {
      // TODO
    },
    draftsByUser: (_parent, args: { id: number }, context: Context) => {
      // TODO
    },
  },
  Mutation: {
    signupUser: (
      _parent,
      args: { name: string | undefined; email: string },
      context: Context
    ) => {
      // TODO
    },
    createDraft: (
      _parent,
      args: { title: string; content: string | undefined; authorEmail: string },
      context: Context
    ) => {
      // TODO
    },
    incrementPostViewCount: (
      _parent,
      args: { id: number },
      context: Context
    ) => {
      // TODO
    },
    deletePost: (_parent, args: { id: number }, context: Context) => {
      // TODO
    },
  },
  Post: {
    author: (parent, _args, context: Context) => {
      return null;
    },
  },
  User: {
    posts: (parent, _args, context: Context) => {
      return [];
    },
  },
  DateTime: DateTimeResolver,
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // 削除 context,

  //変更前 plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
});

//変更前 server.listen({ port: 4000 }, () =>
//変更前   console.log(`🚀 Server ready at: http://localhost:4000`)
//変更前 );
startStandaloneServer(server, {
  context: async () => ({ ...context }),
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`🚀 Server ready at: ${url}`);
});

元のモノにたいして、

// ★ 追加
が追加分

//変更前
とあるのは、変更分で、
そのすぐ下に、変更後のコードを書いてる

// 削除
は、削除分

これらを反映し、

npm run devしたら動く。

動いて起動するのは、動画で説明してる「graphql playground」ではない
よく似た別のものが起動します。
でも、使い方は、ほぼ同じなので、同じようにハンズオンで学習が可能になってる。

上記のものに対して、問題演習後、ひととおり解答が埋まったもの

import { startStandaloneServer } from "@apollo/server/standalone"; // ★ 追加

//変更前 import { ApolloServer } from "apollo-server";
import { ApolloServer } from "@apollo/server";

import { DateTimeResolver } from "graphql-scalars";
import { Context, context } from "./context";

//変更前 import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";
import { argsToArgsConfig } from "graphql/type/definition";

const typeDefs = `
type Query {
  allUsers: [User!]!
  postById(id: Int!): Post
  feed(searchString: String, skip: Int, take: Int): [Post!]!
  draftsByUser(id: Int!): [Post]
}

type Mutation {
  signupUser(name: String, email: String!): User!
  createDraft(title: String!, content: String, authorEmail: String): Post
  incrementPostViewCount(id: Int!): Post
  deletePost(id: Int!): Post
}

type User {
  id: Int!
  email: String!
  name: String
  posts: [Post!]!
}

type Post {
  id: Int!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  content: String
  published: Boolean!
  viewCount: Int!
  author: User
}

scalar DateTime
`;

const resolvers = {
  Query: {
    allUsers: (_parent, _args, context: Context) => {
      // ************************************************************
      // postsをうまく取得するために、
      // 「User: {」の定義を丸ごと削除したうえで、下記の実装でもよい
      // ************************************************************
      // return context.prisma.user.findMany({
      //   include: {
      //     posts: true,
      //   }
      // });
      // ************************************************************

      // postsをうまく取得するためには、「User: {」でのpostsの実装が必要
      return context.prisma.user.findMany();
    },
    postById: (_parent, args: { id: number }, context: Context) => {
      return context.prisma.post.findUnique({
        where: {
          id: args.id,
        },
      })
    },
    feed: (
      _parent,
      args: {
        searchString: string | undefined;
        skip: number | undefined;
        take: number | undefined;
      },
      context: Context
    ) => {
      const or = args.searchString
        ? {
            OR: [
              { title: { contains: args.searchString as string } },
              { content: { contains: args.searchString as string } }
            ]
          }
        : {};

      return context.prisma.post.findMany({
        where: {
          published: true,
          ...or,
        },
        skip: Number(args.skip) || undefined,
        take: Number(args.take) || undefined,
      })
    },
    draftsByUser: (_parent, args: { id: number }, context: Context) => {
      return context.prisma.user.findUnique({
        where: {
          id: args.id,
        }
      }).posts({
        where: {
          published: false,
        }
      })
    },
  },
  Mutation: {
    signupUser: (
      _parent,
      args: { name: string | undefined; email: string },
      context: Context
    ) => {
      return context.prisma.user.create({
        data: {
          email: args.email,
          name: args.name,
        },
      })
    },
    createDraft: (
      _parent,
      args: { title: string; content: string | undefined; authorEmail: string },
      context: Context
    ) => {
      return context.prisma.post.create({
        data: {
          title: args.title,
          content: args.content,
          author: {
            connect: {
              email: args.authorEmail,
            }
          }
        }
      })
    },
    incrementPostViewCount: (
      _parent,
      args: { id: number },
      context: Context
    ) => {
      return context.prisma.post.update({
        where: {
          id: args.id,
        },
        data: {
          viewCount: {
            increment: 1,
          },
        },
      })
    },
    deletePost: (_parent, args: { id: number }, context: Context) => {
      return context.prisma.post.delete({
        where: {
          id: args.id,
        }
      })
    },
  },
  Post: {
    author: (parent, _args, context: Context) => {
      return context.prisma.post.findUnique({
        where: {
          id: parent.id,
        }
      }).author();
    },
  },
  User: {
    posts: (parent, _args, context: Context) => {
      return context.prisma.user.findUnique({
        where: {
          id: parent.id
        }
      }).posts();
    },
  },
  DateTime: DateTimeResolver,
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // 削除 context,

  //変更前 plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
  plugins: [ApolloServerPluginLandingPageLocalDefault()],
});

//変更前 server.listen({ port: 4000 }, () =>
//変更前   console.log(`🚀 Server ready at: http://localhost:4000`)
//変更前 );
startStandaloneServer(server, {
  context: async () => ({ ...context }),
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`🚀 Server ready at: ${url}`);
});

GraphQLで、parentや、sourceの言葉の意味の件

parentというキーワードが出てきて、はじめ混乱していた

<勘違いしてたこと>
DBの外部キーの親子関係の親のことでparentなのか?と思ってしまっていた。( これ、間違い )
参照先の親テーブルの親
参照元の子テーブルの子
parentが、その親のことをではないかと、勘違いしてた ( 間違ってました )

GraphQLでparentというのは、上記の意味ではなくて、
属性を保持している所有者のインスタンスをparentや、sourceという言葉で表現しているとのこと

例として、下記のコードがある

  Post: {
    author: (parent, _args, context: Context) => {
      return context.prisma.post.findUnique({
        where: {
          id: parent.id,
        }
      }).author();
    },
  },
  User: {
    posts: (parent, _args, context: Context) => {
      return context.prisma.user.findUnique({
        where: {
          id: parent.id
        }
      }).posts();
    },
  },

Post: {
author: (parent, _args, context: Context) => {
に出てくるparentは、Postのインスタンス自身のことを言っており
クエリの中で、Postのインスタンスよりauthorの属性が参照された時
( GraphQLは、クエリを送信する側が自由に書けるから )

authorの属性値を、Postのインスタンス自身の属性を使って
返すような実装をするために、Postのインスタンス自身がparentという名前の引数で
渡ってくる。
そういう意味でした。

引数のparent、つまり、Post自身のインスタンス自身の属性値を使って
authorの属性値を求めるクエリを実装する箇所である。

このparentをDBの外部キーの親子関係と勘違いし、親のUserのインスタンスがparentにくるのか?
みたいに、勘違いしてしまい、実装をこねくり回しても、思うようにならず、
ハマってしまった経緯があります・・・。
( 解答見ても、意味がわからず、しばらく混乱してた )

ですので、同じ、ハマリどころに、ハマらないように
「お知らせ」ということで、参考までに、書かせていただきました。

他、補足事項

環境構築時、
動画では、
npx prisma migrate dev --name init
をしてから
npx prisma db seed --preview-feature
をすると解説している箇所がある

やってみると
npx prisma db seed --preview-feature
で、一意制約違反でエラーになってしまう

どうやら
npx prisma migrate dev --name init
をした時点で
npx prisma db seed --preview-feature
に相当するものが、動いてるようです。

npx prisma migrate dev --name init
のログを見たら、そうなっていた。

改まって
npx prisma db seed --preview-feature
をしなくても、
prisma studioで見た時に、シードのデータが入っていた

なんの環境の違いかは不明です。
動画を作ってた時は、
npx prisma migrate dev --name init
npx prisma db seed --preview-feature
の両方が必要だったのか?

私と同じように、
npx prisma migrate dev --name init
の時点で、シードのデータが入って
npx prisma db seed --preview-feature
もやれば、一意制約違反になるかもしれません。

npx prisma migrate dev --name init
だけで十分かもしれません。

他の人が、やって、
そうなるのか、そうならないのか、不明です。

同じ現象となったときに、悩まないで済むように

「そんなこともありました」という「お知らせ」として、
参考までに、一応、書いておきました。

Discussion