Prisma可选关联下品牌/颜色/尺寸删除限制失效问题
问题描述
我使用Prisma、NextJS和Supabase搭建电商管理后台,希望让品牌、颜色、尺寸等产品筛选项在创建产品时设为可选。但设置为可选后,校验这些筛选项是否被产品关联的删除限制功能失效了。例如分类为必填项,用户删除分类前必须先删除所有关联该分类的产品;我需要让品牌、颜色、尺寸也保持同样的规则——关联产品未删除前无法删除这些筛选项。
现有代码
schema.prisma
model Product { id String @id @default(uuid()) storeId String store Store @relation("StoreToProduct", fields: [storeId], references: [id]) categoryId String category Category @relation("CategoryToProduct", fields: [categoryId], references: [id]) name String price Decimal isFeatured Boolean @default(false) isArchived Boolean @default(false) sizeId String? size Size? @relation(fields: [sizeId], references: [id]) colorId String? color Color? @relation(fields: [colorId], references: [id]) brandId String? brand Brand? @relation(fields: [brandId], references: [id]) images Image[] saleItems SaleItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([storeId]) @@index([categoryId]) @@index([brandId]) @@index([sizeId]) @@index([colorId]) }
product.form.tsx
"use client"; import { UseOrigin } from "@/hooks/use-origin"; import { zodResolver } from "@hookform/resolvers/zod"; import { Brand, Category, Color, Image, Product, Size } from "@prisma/client"; import axios from "axios"; import { useParams, useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import * as z from "zod"; const formSchema = z.object({ name: z.string().min(1), images: z.object({ url: z.string() }).array(), price: z.coerce.number().min(1), categoryId: z.string().min(1), colorId: z.string().min(1).optional(), sizeId: z.string().min(1).optional(), brandId: z.string().min(1).optional(), isFeatured: z.boolean().default(false).optional(), isArchived: z.boolean().default(false).optional(), }); interface ProductFormProps { initialData: | (Product & { images: Image[]; }) | null; categories: Category[]; colors?: Color[]; sizes?: Size[]; brands?: Brand[]; } type ProductFormValues = z.infer<typeof formSchema>; export const ProductForm: React.FC<ProductFormProps> = ({ initialData, categories, brands, colors, sizes, }) => { const params = useParams(); const router = useRouter(); const origin = UseOrigin(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const form = useForm<ProductFormValues>({ resolver: zodResolver(formSchema), defaultValues: initialData ? { ...initialData, price: parseFloat(String(initialData?.price)), brandId: initialData.brandId || undefined, colorId: initialData.colorId || undefined, sizeId: initialData.sizeId || undefined, } : { name: "", images: [], price: 0, categoryId: "", colorId: undefined, sizeId: undefined, brandId: undefined, isFeatured: false, isArchived: false, }, });
删除逻辑(color-form.tsx / cell-actions.tsx)
// color-form.tsx 中的删除函数 const onDelete = async () => { try { setLoading(true); await axios.delete(`/api/${params.storeId}/colors/${params.colorId}`); router.refresh(); router.push(`/${params.storeId}/colors`); toast.success("Color deleted."); } catch (error) { toast.error("Make sure you removed all products using this color first"); } finally { setLoading(false); setOpen(false); } }; // cell-actions.tsx 中的删除函数 const onDelete = async () => { try { setLoading(true); await axios.delete(`/api/${params.storeId}/colors/${data.id}`); router.refresh(); toast.success("Color deleted."); } catch (error) { toast.error("Make sure you removed all products using this color first"); } finally { setLoading(false); setOpen(false); } };
解决方案
1. 完善Prisma模型的反向关系
确保Size、Color、Brand模型中配置与Product的反向关联,显式设置删除约束为RESTRICT(默认即为该值,显式声明更清晰):
// Size模型示例,Color、Brand模型同理 model Size { id String @id @default(uuid()) name String storeId String store Store @relation(fields: [storeId], references: [id]) products Product[] // 反向关联产品 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([storeId]) }
2. 后端API添加关联检查
在删除颜色、品牌、尺寸的API路由中,先检查是否有产品关联该选项,有则返回错误,无则执行删除。以颜色为例:
// app/api/[storeId]/colors/[colorId]/route.ts import { prisma } from '@/lib/prisma'; import { NextResponse } from 'next/server'; export async function DELETE( req: Request, { params }: { params: { storeId: string; colorId: string } } ) { try { const { storeId, colorId } = params; // 检查是否有未归档的关联产品 const relatedProductsCount = await prisma.product.count({ where: { storeId, colorId, isArchived: false, }, }); if (relatedProductsCount > 0) { return new NextResponse('该颜色仍有关联产品,无法删除', { status: 400 }); } // 执行删除 const deletedColor = await prisma.color.delete({ where: { id: colorId, storeId, }, }); return NextResponse.json(deletedColor); } catch (error) { return new NextResponse('删除失败,请确保已移除所有关联产品', { status: 500 }); } }
品牌和尺寸的API路由复制上述逻辑,替换对应的模型名称即可。
3. 优化前端错误提示
根据后端返回的错误信息动态提示,提升用户体验:
// color-form.tsx 中的onDelete修改 const onDelete = async () => { try { setLoading(true); await axios.delete(`/api/${params.storeId}/colors/${params.colorId}`); router.refresh(); router.push(`/${params.storeId}/colors`); toast.success('颜色已删除'); } catch (error: any) { const errorMessage = error.response?.data || '请先移除所有使用该颜色的产品'; toast.error(errorMessage); } finally { setLoading(false); setOpen(false); } }; // cell-actions.tsx 中的onDelete同理修改
4. 确认前端表单可选配置
当前product.form.tsx中的Zod Schema已将colorId、sizeId、brandId设为optional(),创建产品时可正常跳过这些选项,无需修改。
内容的提问来源于stack exchange,提问作者Jean Mako
相关产品推荐
相关产品推荐

