📍

Next.js + Prisma をコロケーションの考え方で書く

2023/09/15に公開

Next.js の App Router によって React のコンポーネントから直接 Prisma クライアントを用いて SQL のクエリを実行することが現実的になりました。

このときにコロケーションの1つである GraphQL Fragment と同じ考え方で Prisma のクエリを実行できることに気付いたので紹介します。

サンプルコード

次のリポジトリにサンプルコードがあります。
https://github.com/odan-sandbox/nextjs-prisma-colocation-sandbox

モデル

今回の説明に使用するモデルです。
この Post モデルを表示するコンポーネントの実装について考えます。

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
}

愚直な実装

全部の Post をその Post の author の名前と共に表示するコンポーネントを実装すると次のような形になると思います。

PostList.tsx
import { Post, User } from "@prisma/client";

type Props = {
  posts: (Post & { author: User })[];
};

export const PostList = ({ posts }: Props) => {
  return (
    <div>
      <h1>Post List</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.author.name}: {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

親コンポーネントからは次のように使用します。

export default async function Home() {
  const posts = await prisma.post.findMany({ include: { author: true } });
  return (
    <div>
      <PostList posts={posts} />
    </div>
  );
}

この実装だと posts の型定義が子コンポーネントにあるのに対して、データを取得するクエリは親コンポーネントにあるため責務が分散してしまっています。

Fragment を参考にした実装

prisma.post.findMany の引数に渡す { include: { author: true } } から返り値の推論ができると、クエリの指定と型定義を子コンポーネントに実装することができそうです。

実は Prisma にはこのような型推論を行うための Utility Types が存在します。
https://www.prisma.io/docs/concepts/components/prisma-client/advanced-type-safety/operating-against-partial-structures-of-model-types#problem-using-variations-of-the-generated-model-type

これを使用すると先程の子コンポーネントは次のように実装することができます。

PostList2.tsx
export const getPostsQuery = Prisma.validator<Prisma.PostDefaultArgs>()({
  include: { author: true },
});

type Post = Prisma.PostGetPayload<typeof getPostsQuery>;

type Props = {
  posts: Post[];
};

export const PostList2 = ({ posts }: Props) => {
  return (
    <div>
      <h1>Post List</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.author.name}: {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

親コンポーネントでは getPostsQuery を import してクエリを発行します。

export default async function Home() {
  const posts2 = await prisma.post.findMany(getPostsQuery);
  return (
    <div>
      <PostList2 posts={posts2} />
    </div>
  );
}

まとめ

GraphQL Fragment を参考に Prisma でもクエリと型定義をコンポーネントに集約する方法を紹介しました。

Discussion