Next.js项目中如何将GraphQL API数据存入PostgreSQL数据库
实现方案:将第三方GraphQL拉取的动漫数据存入自有PostgreSQL数据库
核心安全红线:绝对不要在客户端渲染的组件中直接编写数据库操作逻辑——浏览器端运行的代码会完全暴露给所有访问用户,直接连数据库会泄露凭证,所有Prisma相关操作必须放在服务端执行。
第一步:定义Prisma数据模型,匹配存储字段
首先在schema.prisma中建立对应数据表,注意要加第三方接口的原始ID作为唯一键,用于后续去重,避免重复写入数据:
// prisma/schema.prisma model Anime { id Int @id @default(autoincrement()) anilistId Int @unique // 存储AniList返回的原始媒体ID,用于去重 titlePreferred String // 后续需要存储封面、评分、播出时间等字段,直接在此处新增即可 }
模型定义完成后,执行命令将表结构同步到PostgreSQL:
npx prisma db push
第二步:编写服务端数据同步接口
用Next.js的Route Handler编写跑在服务端的同步逻辑,这里不会泄露任何服务端凭证,示例路径为app/api/sync-anime/route.ts(如果用Pages Router,路径改为pages/api/sync-anime.ts即可):
// app/api/sync-anime/route.ts import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import { gql, ApolloClient, InMemoryCache } from '@apollo/client'; const prisma = new PrismaClient(); // 初始化服务端Apollo客户端,直接请求AniList GraphQL接口 const anilistClient = new ApolloClient({ uri: 'https://graphql.anilist.co', cache: new InMemoryCache(), }); // 和前端使用的GraphQL查询语句完全一致,后续加字段直接修改此处即可 const ANIME_QUERY = gql` query GetFirstsThree{ Page(page: 1, perPage: 3) { media { id title { userPreferred } } } } `; export async function GET() { try { // 从AniList拉取原始数据 const { data } = await anilistClient.query({ query: ANIME_QUERY }); const mediaList = data.Page.media; // 做字段映射,匹配Prisma模型的字段结构 const formattedData = mediaList.map((item: any) => ({ anilistId: item.id, titlePreferred: item.title.userPreferred, })); // 批量写入数据库,遇到已存在的anilistId自动跳过 const insertResult = await prisma.anime.createMany({ data: formattedData, skipDuplicates: true, }); return NextResponse.json({ success: true, insertedCount: insertResult.count, }); } catch (err) { console.error('数据同步失败:', err); return NextResponse.json({ success: false }, { status: 500 }); } }
第三步:改造现有客户端组件,触发同步逻辑
你现有的CSR组件不需要大改,只需要在合适的时机调用上面写的同步接口即可,可以选择页面首次加载时自动触发,也可以加手动同步按钮由用户触发,以下是改造后的示例:
'use client'; // CSR组件必须加的标记 import { useQuery, gql } from "@apollo/client"; import { useEffect } from 'react'; const QUERY = gql` query GetFirstsThree{ Page(page: 1, perPage: 3) { media { id title { userPreferred } } } } `; export default function AnimList() { const { data, loading, error } = useQuery(QUERY); // 页面首次加载时触发一次数据同步入库 useEffect(() => { const runSync = async () => { await fetch('/api/sync-anime'); // 同步完成后如果需要读取自有数据库数据,在此处调用自有查询接口即可 }; runSync(); }, []); if (loading) { return <h2>Loading...</h2>; } if (error) { return null; } const medias = data.Page.media; return ( <div className="mainGrid"> {medias.map((value) => { return ( <p key={value.id}>{value.title.userPreferred}</p> ) })} </div> ); }
关键注意事项
- 不要尝试在客户端组件中直接引入PrismaClient:Prisma依赖Node.js运行环境,无法在浏览器中执行,且会直接泄露数据库连接串,造成严重安全问题
- 必须配置去重规则:将AniList返回的原始ID设为唯一键,配合
createMany的skipDuplicates参数自动过滤已存数据,不需要手动写重复判断逻辑,也不会出现插入报错 - 大批量同步不要绑定页面访问:如果后续需要同步全量AniList数据,不要在用户打开页面时触发,用定时任务工具定期调用同步接口即可,避免拖慢页面加载速度,同时防止触发AniList的接口频率限制
- 数据源切换成本极低:后续如果要完全从自有数据库读数据展示,只需要新增一个查询数据库的服务端接口,把前端Apollo的请求地址换成自有接口即可,现有渲染逻辑完全不需要改动
不想写Route Handler的话,也可以用Next.js Server Action实现入库逻辑,核心要求不变:所有数据库操作必须在服务端执行,不能暴露到客户端。
内容的提问来源于stack exchange,提问作者PazzeG
相关产品推荐
相关产品推荐

