You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 04:36:23