云计算百科
云计算领域专业知识百科平台

HowToGraphQL:用 Prisma Client 将 TypeScript/Apollo GraphQL 服务器接入 SQLite 数据库

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:
https://gitcode.com/gh_mirrors/ho/howtographql

点击查看 免费下载

本文基于 HowToGraphQL 教程仓库(The Fullstack Tutorial for GraphQL)中 TypeScript + Nexus + Apollo Server 后端教程的第 5 章 content/backend/typescript-apollo/5-connecting-server-and-database.md,讲解如何通过 resolver 的 context 参数把 Prisma Client 实例接入 Apollo Server,并把此前基于内存数组的 feed 查询与 post 变更重构为真实数据库读写。读完本篇,你可以独立完成"context 接线 + Nexus 上下文类型声明 + resolver 持久化改造 + 数据持久性验证"这套完整链路,并为后续的认证、用户关联等进阶功能打下基础。

背景:本章在教程中的位置

在 HowToGraphQL 的 typescript-apollo 教程轨道中,本章之前已完成以下工作(对应 1-getting-started.md、2-a-simple-query.md、3-a-simple-mutation.md、4-adding-a-database.md):

  • 用 Nexus(code-first 方式)定义了 Link 对象类型与 feed 查询、post 变更;
  • 用 Apollo Server v3 在 http://localhost:3000/ 启动了 GraphQL 服务;
  • 引入 Prisma 生态(Prisma CLI、Prisma Client、Prisma Migrate),执行 npx prisma init 生成 prisma/schema.prisma,并定义了 Link 模型:

// prisma/schema.prisma
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

generator client {
provider = "prisma-client-js"
}

model Link {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
description String
url String
}

  • 通过 npx prisma migrate dev –name "init" 创建了 SQLite 数据库文件 dev.db(含 Link 表)并自动生成 Prisma Client;
  • 在 src/script.ts 中用 prisma.link.findMany() 和 prisma.link.create() 独立验证过数据库读写。

但此时 GraphQL resolver 仍然依赖 src/graphql/Link.ts 中的内存 links 数组和 idCount 变量:一旦服务器重启,所有"提交"的链接都会丢失。本章要解决的就是这个问题——把 GraphQL 服务器与 SQLite 数据库真正连起来,让数据跨进程生命周期持久化。

核心机制:resolver 的 context 参数

回顾前几章的结论:每个 GraphQL resolver 函数固定接收四个参数——parent(上一级 resolver 的结果)、args(操作参数)、context(上下文对象)、info(执行元信息)。本章聚焦此前尚未展开的 context。

按原文档的准确描述:

  • context 是一个普通的 JavaScript 对象,resolver 链中的每一个 resolver 都可以对它读和写,因此它本质上是 resolver 之间通信的通道;
  • 一个非常实用的特性是:你可以在 GraphQL 服务器初始化时就向 context 写入内容;
  • 由此得出结论:在 ApolloServer 初始化时把一个 PrismaClient 实例挂到 context 上,之后所有 resolver 都能通过 context 参数直接拿到它,从而访问数据库。

这也是本章 frontmatter 中配套测验题的考点:

问题:GraphQL resolver 中的 context 参数有什么作用?

正确答案:It lets resolvers communicate with each other(它让 resolver 之间能够相互通信)。

其余三个干扰项("总能提供数据库访问"、"携带查询参数"、"用于认证")都不准确:context 并不天然绑定数据库,携带参数的是 args,认证只是 context 的常见用途之一(见 6-authentication.md)。

第一步:创建 context.ts 并挂载 PrismaClient

出于模块化考虑,原文档专门用一个 context.ts 文件来承载 context 的初始化。在项目的 src 目录下创建该文件:

# 项目根目录为 hackernews-typescript/
touch src/context.ts

然后在 src/context.ts 中定义 Context 接口并导出 context 对象:

// src/context.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();

export interface Context { // 1
prisma: PrismaClient;
}

export const context: Context = { // 2
prisma,
};

逐条理解:

  • // 1:先定义 Context 接口,声明 context 对象上会挂哪些东西。目前只有一个 PrismaClient 实例,但随着项目增长(例如后续加入用户会话、认证信息),这个接口会继续扩展——这正是 TypeScript"先声明类型、再创建对象"的标准工作流带来的收益;
  • // 2:导出 context 对象,供 GraphQL 服务器(index.ts)导入使用。同时模块顶层导出的 prisma 实例是单例,保证整个进程共享同一个数据库连接池。

第二步:向 Nexus 声明 context 类型

Nexus 是 code-first 的 schema 构建库,它会自动生成 nexus-typegen.ts(TypeScript 类型)与 schema.graphql(SDL),并据此对所有 resolver 签名做类型检查。为了让 Nexus 知道 context 的确切类型,需要修改 src/schema.ts 中的 makeSchema 调用,新增 contextType 配置:

// src/schema.ts
export const schema = makeSchema({
types,
outputs: {
typegen: join(process.cwd(), "nexus-typegen.ts"),
schema: join(process.cwd(), "schema.graphql"),
},
contextType: {
module: join(process.cwd(), "./src/context.ts"), // 1
export: "Context", // 2
},
});

两个选项的含义:

  • // 1 module:导出 context 接口(或类型)的文件(也称作模块)的路径;
  • // 2 export:该模块中导出的接口名。

配置完成后,Nexus 会确保所有 resolver 的 context 参数都匹配 Context 接口——如果在 resolver 里写 context.xxx 而 xxx 不在 Context 中,TypeScript 会直接报错。这与 Nexus"自动生成类型、保证 schema 定义与实现同步"的整体设计一脉相承(该机制在 1-getting-started.md 中已介绍)。

第三步:把 context 接入 ApolloServer

在 src/index.ts 中导入 context 并传入 ApolloServer 构造函数:

// src/index.ts
import { context } from "./context";

export const server = new ApolloServer({
schema,
context,
});

从此,ApolloServer 实例化时,context 对象就带着 PrismaClient 实例(字段名为 prisma)被初始化;所有 resolver 内部都可以用 context.prisma 访问数据库。

第四步:重构 resolver,切换到数据库读写

数据源换成真实数据库后,src/graphql/Link.ts 中的内存 links 数组和 idCount 变量就没有存在意义了,应整体删除——它们只在前一章的内存方案中负责临时存储和生成自增 id,而这两项工作现在分别由 SQLite 表和 @id @default(autoincrement()) 属性接管。

需要更新的 resolver 有两个:feed 查询和 post 变更。

// src/graphql/Link.ts
import { extendType, nonNull, objectType, stringArg } from "nexus";

export const LinkQuery = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field("feed", {
type: "Link",
resolve(parent, args, context) {
return context.prisma.link.findMany(); // 1
},
});
},
});

export const LinkMutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field("post", {
type: "Link",
args: {
description: nonNull(stringArg()),
url: nonNull(stringArg()),
},
resolve(parent, args, context) {
const newLink = context.prisma.link.create({ // 2
data: {
description: args.description,
url: args.url,
},
});
return newLink;
},
});
},
});

两个关键改动:

  • // 1 feed resolver:通过 context.prisma 拿到 Prisma Client 实例,调用 findMany() 查询并返回数据库中全部 Link 记录;
  • // 2 post resolver:对 Link 模型调用 Prisma Client 的 create 方法,把 resolver 从 args 参数中收到的 description 与 url 作为 data 传入,返回新创建的 Link 对象。注意 id 与 createdAt 无需手动填写:schema.prisma 中 @id @default(autoincrement()) 与 @default(now()) 会由数据库/Prisma 自动填充。

注意(异步语义):Prisma 的查询方法返回 Promise 对象,因为数据库访问是异步的。因此上面两个 resolver 实际都是返回一个 Promise。这不是问题——Apollo Server 能够检测 resolver 返回的 Promise 并自动解析其结果,无需在 resolver 内手动 await。

验证:数据已持久化到 SQLite

改造完成后启动服务(npm run dev,即 ts-node-dev –transpile-only –no-notify –exit-child src/index.ts),在 Apollo Studio 中发送与前几章相同的 feed 查询和 post 变更:

# 查询所有链接
query {
feed {
id
url
description
}
}

# 提交一条新链接
mutation {
post(url: "www.prisma.io", description: "Next-generation Node.js and TypeScript ORM") {
id
}
}

与前几章的唯一区别在于:这次提交的链接会被持久化到 prisma/schema.prisma 中 url = "file:./dev.db" 指向的 SQLite 数据库文件里。因此即使杀掉进程、重启服务器,再次执行 feed 查询依然能拿到之前创建的所有链接——这正是本章改造的直接可观测效果,也是与"内存数组方案"的本质分界线。

顺带回顾原文档总结的标准工作流:每当数据发生变化,按"修改 Prisma 数据模型 → 用 prisma migrate 迁移数据库 → (重新)生成 Prisma Client → 在应用代码中使用 Prisma Client"的顺序推进即可。

理解 PrismaClient:由 schema.prisma 生成的 CRUD API

从整体系统视角看,本章补齐了 Prisma/GraphQL 项目的完整闭环:

  • schema.prisma 中的数据模型(如 Link)定义了数据库结构;
  • Prisma Client 基于这些模型自动生成一套类型安全的 CRUD API——PrismaClient 实例可以访问数据库中的所有模型,对每个模型暴露 findMany、create、update、delete 等操作,让你完成创建、读取、更新、删除全链路操作;
  • GraphQL resolver 作为"消费端",利用这套 Prisma Client API 执行 query 与 mutation 对应的数据库操作,并把结果返回给 GraphQL 执行引擎。
  • 换句话说,Prisma Client 就是 schema.prisma 模型定义到应用代码之间的桥梁:模型怎么写,CRUD 方法就长什么样,且全部带 TypeScript 类型(这正是原文档鼓励读者亲手输入代码、利用自动补全探索 API 的原因)。

    后续延伸:context 的扩展价值

    本章只往 context 上挂了一个 prisma,但这个抽象的回报会随项目复杂度上升而放大。紧接着的 6-authentication.md 就给出了范例:在 Prisma 数据模型中新增 User 模型、执行 npx prisma migrate dev –name "add-user-model" 迁移后,src/graphql/User.ts 中的 resolver 依然通过同一个 context.prisma 完成 findUnique 等操作。由于 Context 接口已在 context.ts 中集中声明,未来若需追加会话用户、鉴权头等字段,只需扩展接口并同步 makeSchema 的 contextType 声明,Nexus 的类型检查会覆盖到所有 resolver。

    附录:本篇内容在 HowToGraphQL 仓库中的承载方式

    作为补充说明,本仓库本身是用 Gatsby 构建的教程站点:meta/writing-guidelines.md 规定每章必须是带 frontmatter(title、description、question、answers、correctAnswer)的单个 Markdown 文件,其中后三项定义了章末的选择题;正文中的操作步骤使用 <Instruction> 标签标注,由 src/components/Tutorials/Instruction.tsx 渲染成高亮操作块,章末测验则由 src/components/Quiz/Quiz.tsx 根据 frontmatter 数据渲染。也就是说,你在仓库里读到的这一章,既是教学文本,也是一套被站点组件直接消费的结构化内容。

    小结

    本章完成了 HowToGraphQL typescript-apollo 教程中的关键一步:借助 resolver 的 context 参数,把 Prisma Client 从"脚本里的一次性工具"变成了"服务器全生命周期的共享依赖"。三处改动(context.ts 定义上下文、schema.ts 声明 contextType、index.ts 传入 context)加上 resolver 的两处替换(links 数组 → prisma.link.findMany(),手工 push → prisma.link.create()),即实现了从内存存储到 SQLite 持久化的平滑迁移,同时全程保留了 Nexus + TypeScript 的端到端类型安全。

    赞

    分享

    【免费下载链接】howtographql

    The Fullstack Tutorial for GraphQL

    项目地址:
    https://gitcode.com/gh_mirrors/ho/howtographql

    点击查看 免费下载

    上一篇:
    7个关键步骤打造完美音乐体验:从零开始配置MusicPlayer2全指南

    下一篇:
    极致音频体验:5步掌握MusicPlayer2全能播放器

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » HowToGraphQL:用 Prisma Client 将 TypeScript/Apollo GraphQL 服务器接入 SQLite 数据库
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!