全栈开发, 精选文章

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 组件重新获取数据,而是:

  1. React Query 重新执行 useCategories() 查询
  2. useCategories() 返回新的数据状态
  3. 使用这个 Hook 的组件(CategoryCards)响应式地重新渲染
  4. 组件使用新数据进行渲染

关键区别: 这是 React 的状态驱动更新机制,不是组件主动刷新!

🧠 React 的智能更新

// React 很聪明,它:
// 1. 知道哪些数据变了
// 2. 只更新相关的 DOM 元素
// 3. 不重新渲染整个页面
// 4. 保持其他组件的状态不变