文章
next js 数据查询示例
一、工具
1、统一异常处理
import { Prisma } from "@prisma/client"; // 使用prisma默认配置没有指定output
import { fromError } from "zod-validation-error";
import { ZodError } from "zod";
import { AuthError } from "next-auth";
const PRISMA_ERROR_CODES = new Map<string, string>([
[
"P2000",
"The provided value for the column is too long for the column's type"
],
["P2001", "The record searched for in the where condition does not exist"],
["P2002", "Unique constraint failed"],
["P2003", "Foreign key constraint failed"],
["P2004", "A constraint failed on the database"],
[
"P2005",
"The value stored in the database for the failed is invalid for the field's type"
],
["P2006", "The provided value for the field is not valid"],
["P2007", "Data validation error"],
["P2008", "Failed to parse the query"],
["P2009", "Failed to validate the query"],
["P2010", "Raw query failed"],
["P2011", "Null constraint violation"],
["P2012", "Missing a required argument"],
[
"P2014",
"The change you are tring to make would violate the required relation"
],
["P2015", "A related record could not be found"],
["P2016", "QUery interpretetion error"],
[
"P2017",
"The records for relation between the parent and child models are not connected"
],
["P2018", "The required connected records were not found"],
["P2019", "Input error"],
["P2020", "Value out of range for the type"],
["P2021", "The table does not exists in the current database"],
["P2022", "The column does not exist in the current database"],
["P2023", "Inconsistent column meta"],
["P2024", "Timed out fetching a new connection from the pool"],
[
"P2025",
"An operation failed because it depends on one or more records that were required but not found"
],
[
"P2026",
"The current database provider doesn't support a feature that query used"
],
["P2027", "Multiple errors occurred on the database suring query execution"]
]);
const getErrorMessage = (error: unknown): string => {
if (error instanceof AuthError) {
return "Wrong credentials or the user did not found."
} else if (error instanceof ZodError) {
const message = fromError(error)
if (message) {
return message.toString();
}
}
if (error instanceof Prisma.PrismaClientKnownRequestError) {
const errorCode = error.code;
const message = PRISMA_ERROR_CODES.get(errorCode);
if (message) {
return message;
}
if (errorCode === "P2002") {
const field = (error.meta?.target as string[])?.[0] || "field";
return `A record with this ${field} already exists.`;
}
}
if (error instanceof Prisma.PrismaClientValidationError) {
return "Invalid data provided."
}
return "An unexpected error occurred.";
}
export { getErrorMessage };2、执行函数
import { getErrorMessage } from "./getErrorMessage";
import { isRedirectError } from "next/dist/client/components/redirect-error";
// 如果 actionFn 是 () => db.category.findMany()
// 那么 T 就是 Category[] 类型
// 如果 actionFn 是 () => db.category.delete({ where: { id: 1 } })
// 那么 T 就是 Category 类型
// T 泛型参数 标识函数的返回值类型
type Options<T> = {
// 一个函数,没有参数 返回Promise,Promise类型是T
actionFn: () => Promise<T>;
}
const executeAction = async <T>({ actionFn }: Options<T>) => {
try {
await actionFn(); // 参数是lambda形式显示传进去的所以这里没有参数
} catch (error) {
if (isRedirectError(error)) {
throw error;
}
throw new Error(getErrorMessage(error));
}
}
export { executeAction };二、数据查询
1、查询
"use server";
import db from "@/lib/client";
const getCategories = async () => {
return await db.category.findMany();
}
export { getCategories };2、修改
"use server";
import db from "@/lib/client";
import { executeAction } from "@/lib/executeAction";
const deleteCategory = async (id: number) => {// id 在这里被记住了
await executeAction({ // 自定义执行服务 会捕捉自定义的错误
// 这个函数记住了id
actionFn: () => db.category.delete({ where: { id } }), // 这个箭头函数"捕获"了外部的 id 变量
});
};
export { deleteCategory };上面函数等价于
const deleteCategory = async (id: number) => { // id 参数每次不同
// 1. 创建新的 actionFn 函数(动态创建)
const actionFn = () => db.category.delete({ where: { id } });
// ↑ 记住当前的 id
// 2. 传给 executeAction
await executeAction({
actionFn: actionFn // 每次都是不同的函数实例
});
};
// 假设有以下调用:
deleteCategory(1); // 第一次调用
deleteCategory(5); // 第二次调用
deleteCategory(10); // 第三次调用
// 每次都会创建不同的 actionFn:
// 第一次调用时:
const actionFn1 = () => db.category.delete({ where: { id: 1 } });
// 第二次调用时:
const actionFn2 = () => db.category.delete({ where: { id: 5 } });
// 第三次调用时:
const actionFn3 = () => db.category.delete({ where: { id: 10 } });
// 每个 actionFn 都是不同的函数,"记住"了不同的 id 值因为这里是动态创建了函数 所以外部可以直接执行actionFn() 无参的函数,是因为参数已经在函数动态定义的时候传进去了
三、Hook操作
1、查询
import { useQuery } from "@tanstack/react-query"
import { getCategories } from "./categoryQueries"
const useCategories = () => {
return useQuery({
queryKey: ["categories"], // 查询的唯一标识
queryFn: getCategories, // 执行查询的函数
});
};
export { useCategories };也是闭包
// 以上是无参数的闭包
// 下面给出有参数闭包示例
const useCategories = (storeId?: number) => {
return useQuery({
queryKey: ["categories", storeId],
queryFn: () => getCategories(storeId), // 闭包:记住 storeId
});
};2、修改
import { toast } from "sonner";
import { deleteCategory } from "./categoryMutations";
import { useMutation, useQueryClient } from "@tanstack/react-query";
const useDeleteCategory = () => {
// 获取查询客户端
const queryClient = useQueryClient();
// useMutation 专门处理数据变更操作(创建、更新、删除)
return useMutation({
mutationFn: async (id: number) => { // 真正执行的变更数据函数
await deleteCategory(id);
},
onSuccess: () => {
// 显示成功消息
toast.success("Category deleted successfully");
// 更新缓存数据
// invalidateQueries 告诉 React Query:"categories" 这个查询的缓存数据已经过期了"
// React Query 会自动重新执行 useCategories() 查询
queryClient.invalidateQueries({ queryKey: ["categories"] });
}
})
};
export { useDeleteCategory };四、组件操作
"use client";
import { Button } from "@/components/ui/button";
import { useDeleteCategory } from "../_services/use-category-mutations";
import { useCategories } from "../_services/use-category-queries"
import { Edit, Trash } from "lucide-react";
const CategoryCards = () => {
// 当组件渲染时:React Query:
// 1. 组件首次渲染 → useCategories() 被调用
// 2. useQuery 检查缓存 → 发现没有 ["categories"] 缓存
// 3. React Query 调用 getCategories() → 执行数据库查询
// 4. 数据返回 → 更新缓存和组件状态
// 5. 组件重新渲染 → 显示数据
// 6. 后续渲染 → useCategories() 再次被调用
// 7. useQuery 检查缓存 → 发现有有效缓存
// 8. 直接返回缓存数据 → 组件快速渲染
const categoriesQuery = useCategories(); // 获取分类查询
// 1. 用户点击删除按钮
// 2. deleteCategoryMution.mutate(item.id) 执行
// 3. mutationFn: async (id: number) => { await deleteCategory(id); } 开始执行
// 4. deleteCategory(id) → 服务器执行删除操作
// 5. 服务器返回成功响应
// 6. onSuccess 回调执行:
// - toast.success("Category deleted successfully")
// - queryClient.invalidateQueries({ queryKey: ["categories"] })
// 7. React Query 重新执行 useCategories() 查询
// 8. 分类列表自动更新
const deleteCategoryMution = useDeleteCategory(); // 获取删除变更
return (
<div className="grid grid-cols-4 gap-2">
{categoriesQuery.data?.map((item) => ( // 显示分类数据
<div
className="bg-accent flex flex-col justify-between gap-3 rounded-lg p-6 shadow-md"
key={item.id}
>
<p className="truncate">{item.name}</p>
<div className="flex gap-1">
<Button
className="size-6"
variant="ghost"
size="icon"
onClick={() => { }}
>
<Edit />
</Button>
<Button
className="size-6"
variant="ghost"
size="icon"
onClick={() => {
deleteCategoryMution.mutate(item.id);
}}
>
<Trash />
</Button>
</div>
</div>
))}
</div>
)
}
export { CategoryCards };✅ React Query + React 状态更新:
1. React Query 更新 useCategories() 的返回值
2. 使用 useCategories() 的组件检测到状态变化
3. 组件重新渲染(局部更新)
4. 只更新相关的 UI 部分🧠 实际代码流程
// 1. 删除操作
deleteCategoryMution.mutate(item.id);
// 2. 成功后使缓存失效
queryClient.invalidateQueries({ queryKey: ["categories"] });
// 3. React Query 重新执行查询
// getCategories() 返回新数据(不包含已删除项)
// 4. useCategories() Hook 返回新状态
const categoriesQuery = {
data: [/* 新数组,已移除删除的项 */],
// ...
};
// 5. CategoryCards 组件重新渲染,使用新数据
// ❗ 这是 React 的状态驱动更新,不是组件刷新不是 Card 组件重新获取数据,而是:
- React Query 重新执行
useCategories()查询 useCategories()返回新的数据状态- 使用这个 Hook 的组件(CategoryCards)响应式地重新渲染
- 组件使用新数据进行渲染
关键区别: 这是 React 的状态驱动更新机制,不是组件主动刷新!
🧠 React 的智能更新
// React 很聪明,它:
// 1. 知道哪些数据变了
// 2. 只更新相关的 DOM 元素
// 3. 不重新渲染整个页面
// 4. 保持其他组件的状态不变