Database

使用数据库客户端

如何在服务端组件、API 路由和多租户上下文中通过 Prisma v7 客户端查询 PostgreSQL。

Nebutra 从 @nebutra/db 导出一个预配置的 Prisma 客户端。始终从该包导入 — 永远不要在应用程序代码中直接实例化 PrismaClient

import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  }).$extends(withAccelerate());

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

服务端组件查询

直接在 React 服务端组件中获取数据。内部数据访问不需要 API 层。

import { db } from "@nebutra/db";

export default async function ProjectsPage() {
  const projects = await db.project.findMany({
    orderBy: { createdAt: "desc" },
    select: {
      id: true,
      name: true,
      createdAt: true,
      _count: { select: { members: true } },
    },
  });

  return <ProjectList projects={projects} />;
}

服务端组件在服务器上运行,因此数据库访问是安全的。永远不要在客户端组件("use client" 文件)中导入 @nebutra/db — 数据库凭据会暴露给浏览器。

多租户查询

在多租户上下文中,查询前用 withRls 包装 db 客户端。这会激活 PostgreSQL 行级安全,确保查询自动限定在当前租户范围内。

import { db } from "@nebutra/db";
import { getCurrentTenant, withRls } from "@nebutra/tenant";

export default async function ProjectsPage() {
  const tenant = getCurrentTenant();
  const tenantDb = withRls(db, tenant.tenantId);

  // 只返回当前租户的项目
  const projects = await tenantDb.project.findMany({
    orderBy: { createdAt: "desc" },
  });

  return <ProjectList projects={projects} />;
}

对于涉及租户所有数据的任何查询,始终使用 withRls(db, tenantId)。不使用 RLS 查询将返回所有租户的数据。

客户端数据获取

客户端组件不能导入 @nebutra/db。通过 API 路由获取数据:

"use client";

import { useEffect, useState } from "react";

export function useProjects() {
  const [projects, setProjects] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/projects")
      .then((res) => res.json())
      .then((data) => {
        setProjects(data.data);
        setLoading(false);
      });
  }, []);

  return { projects, loading };
}
import { db } from "@nebutra/db";
import { getCurrentTenant, withRls } from "@nebutra/tenant";
import { NextResponse } from "next/server";

export async function GET() {
  const tenant = getCurrentTenant();
  const tenantDb = withRls(db, tenant.tenantId);

  const projects = await tenantDb.project.findMany({
    orderBy: { createdAt: "desc" },
  });

  return NextResponse.json({ success: true, data: projects });
}

事务

当多个写操作必须一起成功或失败时,使用 Prisma 交互式事务:

import { db } from "@nebutra/db";

async function createOrganizationWithOwner(name: string, userId: string) {
  return db.$transaction(async (tx) => {
    const org = await tx.organization.create({
      data: { name },
    });

    await tx.membership.create({
      data: {
        organizationId: org.id,
        userId,
        role: "OWNER",
      },
    });

    await tx.auditLog.create({
      data: {
        organizationId: org.id,
        action: "organization.created",
        actorId: userId,
      },
    });

    return org;
  });
}

错误处理

Prisma 抛出类型化错误。显式处理它们以返回有意义的响应:

import { Prisma } from "@prisma/client";

export function handlePrismaError(error: unknown): never {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    switch (error.code) {
      case "P2002":
        // 唯一约束违反
        throw new Error(`该值的记录已存在。字段:${error.meta?.target}`);
      case "P2025":
        // 记录未找到
        throw new Error("请求的记录不存在。");
      case "P2003":
        // 外键约束失败
        throw new Error("此操作引用了一条不存在的记录。");
      default:
        throw new Error(`数据库错误:${error.code}`);
    }
  }

  if (error instanceof Prisma.PrismaClientValidationError) {
    throw new Error("提供给数据库查询的数据无效。");
  }

  throw error;
}

常见 Prisma 错误代码:

代码含义
P2002唯一约束违反
P2003外键约束失败
P2025记录未找到(findUniqueOrThrowupdatedelete
P2016查询解释错误
P1001无法连接到数据库服务器
P1002数据库服务器超时

类型化查询

Prisma v7 为每个模型和查询生成完整的 TypeScript 类型。直接使用生成的类型:

import type { Prisma } from "@prisma/client";

// 从特定查询形状推断返回类型
type ProjectWithCount = Prisma.ProjectGetPayload<{
  include: {
    _count: { select: { members: true } };
  };
}>;

// 用于组件 props
interface ProjectCardProps {
  project: ProjectWithCount;
}

使用 Prisma.XxxGetPayload 从查询的 selectinclude 形状派生精确类型。这避免了维护可能与架构偏离的手动接口定义。

How is this guide?

目录