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() を採用しました。 ボイラープレートが多いと実装ミスも増え、レビューも大変になるため、プラグインを書いて正解でした。正式リリースされたら積極的にプロジェクトで使っていきたいです。
