ZenStack v3とプラグインでNext.jsのキャッシュ管理をスッキリさせる

2025-12-21T06:14:39.822Z

はじめに

Next.js + ZenStackの構成が気に入って使っています。特にアクセスポリシーベースでデータ取得ができるところがお気に入りです。

そんなZenStackですが、最近 v3-beta で遊んでいます。現状v3ベータはまだ機能不足な面もありますが、新しいプラグインシステムが導入され、PrismaからKyselyに置き換わったことでクエリの柔軟性も上がり、とても良い感じです。

今回の例で扱うモデル

model User {
  id            String    @id @default(cuid())
  name          String
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  posts         Post[]
}

model Post {
  id            String    @id @default(cuid())
  authorId      String
  title         String
  body          String
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  author        User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
}

Next.jsでのデータキャッシュ

Next.jsを使うなら、キャッシュは最大限効かせつつ、最新のデータをできる限り早く表示したいですよね。データが更新されたとき、どの画面をrevalidate(再検証)するのかを考える必要があります。

最近は 'use cache' も登場しましたが、キャッシュのpurge(破棄)漏れで反映が一部だけ遅れるといったことが起きがちです。
例えば:

  • ユーザーが名前を更新 → ユーザープロフィール画面だけ更新され、記事詳細画面の投稿者名が古いまま
  • 記事のタイトルを更新 → 記事詳細画面のタイトルだけ更新され、記事一覧が更新されない

そのうち有効期限が切れて正しい状態にはなりますが、UXとしては気になります。最終的にとった策は、「データキャッシュは適切に管理して長めに、ルートキャッシュは短めに」というパターンでした。

ZenStack v2 時代のデータキャッシュ

v2までは、Prismaから取得したデータをキャッシュできる形に変換して unstable_cache() を使う必要がありました。キャッシュに使うキーやタグの設計に悩むことになります。

'use server'

export async function getPosts() {
  const enhancePrisma = enhance(prisma, { user })
  // unstable_cache でキャッシュを作る
  const cachedGetPosts = unstable_cache(
    async () => {
      const posts = await enhancePrisma.post.findMany({
        include: { author: true },
      })
      // Date型はキャッシュできないため文字列に変換
      return posts.map(post => ({
        ...post,
        createdAt: post.createdAt.toISOString(),
      }))
    },
    ['posts:list'], // キャッシュキー設定
    {
      // タグ設定 autherまで見ているためUserテーブルの更新時でもrevalidate(再検証)したい
      tags: ['posts', 'users'],
    }
  )
  return await cachedGetPosts()
}

export async function createPost(authUser, postData) {
  const enhancePrisma = enhance(prisma, { authUser })
  const post = await enhancePrisma.post.create(postData);
  
  revalidateTag('posts') // 更新したテーブルのタグをRevalidate(再検証)

  return post
}

export async function updateUserName(authUser, name) {
  const enhancePrisma = enhance(prisma, { authUser })
  const user = await enhancePrisma.user.update({
    where: {
      id: authUser.id
    },
    data: {
      name,
    },
  })

  // 更新したテーブルのタグをRevalidate(再検証)
  // getPostsで`users`タグを登録忘れると更新できない
  revalidateTag('users')  

  return user;
}

ZenStack v3 時代のデータキャッシュ

プラグインによるデータキャッシュ ZenStack ORMのプラグインシステム(公式ドキュメント)を利用します。今回は自作のORMプラグインを作成しました。

submoduleなどでプロジェクト内に配置して schema.zmodel に設定すれば使えます。

plugin cache {
  provider = './src/zenstack/plugins/nextjs-cache/plugin.zmodel'
}

プラグイン有効化

// キャッシュプラグイン
const cachedDb = db.$use(createNextjsCachePlugin())
// ポリシープラグイン
const authDb = cachedDb.$use(new PolicyPlugin())

'use server'

export async function getPosts() {
  return await cachedDb.post.findMany({
    include: { author: true }
  })
}

export async function createPost(authUser, postData) {
  const userDb = authDb.$setAuth(authUser)
  const post = await userDb.post.create(postData);

  return post
}

export async function updateUserName(authUser, name) {
  const userDb = authDb.$setAuth(authUser)
  const user = await userDb.user.update({
    where: {
      id: authUser.id
    },
    data: {
      name,
    },
  })

  return user;
}

コードが超スッキリしました。

おわりに

本当はプラグイン内で 'use cache' を使ってキャッシュ処理を書きたかったのですが、まだ使用できなかったため unstable_cache() を採用しました。 ボイラープレートが多いと実装ミスも増え、レビューも大変になるため、プラグインを書いて正解でした。正式リリースされたら積極的にプロジェクトで使っていきたいです。

Miyulab