如何使用 Apollo-Server 和 Prisma 构建 GraphQL API
💻 📓 📕 📗 📘 📙 📔 📒 📚 📖 💙 💜 💓 💗 💖 💘 💝 🎁 🎊 🎉
介绍
GraphQL 是一种查询语言,旨在通过提供直观灵活的语法和系统来构建客户端应用程序,从而描述其数据需求和交互。在上一课中,您学习了如何结合使用 GraphQL 和Prisma,因为它们的功能相辅相成。
在本课中,您将学习如何处理具有复杂关系的多个模型,这些模型能够真实地反映业务需求。
内容
🔷 步骤 1 — 创建 Node.js 项目
🔷 步骤 2 — 使用 PostgreSQL 设置 Prisma
🔷 第三步 — 使用 Prisma 创建和迁移数据库
🔷 第 4 步 — 定义 GraphQL 模式
🔷 第 5 步 — 定义 GraphQL 解析器
🔷 第 6 步 — 创建 GraphQL 服务器
🔷 第 7 步 — 测试和部署
先决条件
- 完成上一课
🔷 步骤 1 — 创建 Node.js 项目
首先,为你的项目创建一个新目录,初始化 npm 并安装依赖项:
$ mkdir node-graphql-lesson-04
$ cd node-graphql-lesson-04
$ npm init --yes
$ npm install apollo-server graphql
-
Apollo Server: Apollo Server是一个由社区维护的开源 GraphQL 服务器,兼容任何 GraphQL 客户端。它是构建可用于生产环境、自文档化的 GraphQL API 的最佳方式,该 API 可以使用来自任何来源的数据。
-
graphql: GraphQL.js是 GraphQL 的 JavaScript 参考实现。它提供了两个重要功能:构建类型模式和针对该类型模式提供查询服务。
您已创建项目并安装了依赖项。下一步,您需要定义 GraphQL schema,它决定了 API 可以处理的操作。
🔷 步骤 2 — 使用 PostgreSQL 设置 Prisma
Prisma schema 是 Prisma 设置的主要配置文件,其中包含数据库架构。
首先使用以下命令安装 Prisma CLI:
$ npm install prisma -D
Prisma CLI 可以帮助处理数据库工作流程,例如运行数据库迁移和生成 Prisma 客户端。
接下来,您将使用 Docker 设置 PostgreSQL 数据库。使用以下命令创建一个新的 Docker Compose 文件:
$ touch docker-compose.yml
现在将以下代码添加到新创建的文件中:
# node-graphql-lesson-04/docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:13
restart: always
environment:
- POSTGRES_USER=db_user
- POSTGRES_PASSWORD=db_password
volumes:
- postgres:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres:
此 Docker Compose 配置文件负责在您的机器上启动官方 PostgreSQL Docker 镜像。POSTGRES_USER 和 POSTGRES_PASSWORD 环境变量设置超级用户(具有管理员权限的用户)的凭据。您还将使用这些凭据将 Prisma 连接到数据库。最后,您需要定义一个卷,PostgreSQL 将在其中存储数据,并将您机器上的 5432 端口绑定到 Docker 容器中的相同端口。
完成以上设置后,请使用以下命令启动 PostgreSQL 数据库服务器:
$ docker-compose up -d
PostgreSQL 容器运行后,现在可以创建 Prisma 配置了。从 Prisma CLI 运行以下命令:
$ npx prisma init
# node-graphql-lesson-04/prisma/.env
DATABASE_URL="postgresql://db_user:db_password@localhost:5432/college_db?schema=public"
🔷 第三步 — 使用 Prisma 创建和迁移数据库
您的大学 GraphQL API 目前只有一个名为Student 的实体。在此步骤中,您将通过在 Prisma schema 中定义一个新模型并调整 GraphQL schema 以使用该新模型来扩展 API。您将引入Teacher、Course和Department模型。此外, Department模型与Student模型之间以及Teacher与Course 模型之间都存在一对多关系。这将允许您表示一位教师和一门课程,例如,将多门课程与一位教师关联起来。然后,您将扩展 GraphQL schema,以允许通过 API 创建教师并将课程与教师关联起来。
首先,打开 Prisma schema 并添加以下内容:
大学管理系统基本应包含以下几个方面:
- 学生
- 教师
- 部门
- 课程
其他实体,例如课程、费用、成绩单和班级,显然也是解决方案的一部分,但就本课而言并非必要。请参见下面的实体图:
前往 node-graphql/prisma/schema.prisma 文件,并向其中添加以下模型定义:
//* node-graphql-lesson-04/prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Student {
id Int @id @default(autoincrement())
email String @unique @db.VarChar(255)
fullName String? @db.VarChar(255)
enrolled Boolean @default(false)
dept Department @relation(fields: [deptId], references: [id])
deptId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map(name: "student")
}
model Department {
id Int @id @default(autoincrement())
name String @unique
description String? @db.VarChar(500)
students Student[]
courses Course[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map(name: "department")
}
model Teacher {
id Int @id @default(autoincrement())
email String @unique @db.VarChar(255)
fullName String? @db.VarChar(255)
courses Course[]
type TeacherType @default(FULLTIME)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map(name: "teacher")
}
model Course {
id Int @id @default(autoincrement())
code String @unique
title String @db.VarChar(255)
description String? @db.VarChar(500)
teacher Teacher? @relation(fields: [teacherId], references: [id])
teacherId Int?
dept Department? @relation(fields: [deptId], references: [id])
deptId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map(name: "course")
}
enum TeacherType {
FULLTIME
PARTTIME
}
您已将以下内容添加到 Prisma 架构中:
- 系部模型用于表示课程专业方向。
- 教师模型用于表示课程讲师/辅导员。
- 课程模型用于表示学科内容
学生模型已修改如下:
-
两个关系字段:dept 和 deptId。关系字段定义了 Prisma 层级模型之间的连接,它们并不存在于数据库中。这些字段用于生成 Prisma 客户端,以及通过 Prisma 客户端访问关系。
-
deptId 字段由 @relation 属性引用。Prisma 将在数据库中创建一个外键,用于连接学生和部门。
请注意,学生模型中的“系”字段是可选的,与课程模型中的“教师”字段类似。这意味着您可以创建未关联任何系的学生,也可以创建没有关联教师的课程。
这种关系是有道理的,因为课程通常会分配给教师,而且注册学生通常会被分配到某个系。
接下来,使用以下命令在本地创建并应用迁移:
$ npx prisma migrate dev
如果迁移成功,您将收到以下内容:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "college_db", schema "public" at "localhost:5432"
Database reset successful
The following migration(s) have been applied:
migrations/
└─ 20210821201819_init/
└─ migration.sql
✔ Generated Prisma Client (2.29.1) to ./node_modules/@prisma/client in 109ms
该命令还会生成 Prisma 客户端,以便您可以使用新表和字段。
现在,您需要更新 GraphQL schema 和解析器,以使用更新后的数据库 schema。
模型构建完成后,即可使用 Prisma Migrate 在数据库中创建相应的表。这可以通过 `migrate dev` 命令完成,该命令会创建迁移文件并运行它们。
再次打开终端并运行以下命令:
$ npx prisma migrate dev --name "init"
您现在已经创建了数据库架构。接下来,您将安装 Prisma 客户端。
Prisma Client 是一个自动生成且类型安全的对象关系映射器 (ORM),您可以使用它从 Node.js 应用程序以编程方式读取和写入数据库中的数据。在此步骤中,您将在项目中安装 Prisma Client。
再次打开终端并安装 Prisma Client npm 包:
$ npm install @prisma/client
数据库和 GraphQL schema 创建完毕,Prisma Client 也已安装完成。现在,您将在 GraphQL 解析器中使用 Prisma Client 来读写数据库中的数据。具体操作是替换 database.js 文件的内容,该文件目前用于存储您的数据。
//* node-graphql-lesson-04/src/database.js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient();
module.exports = {
prisma,
}
接下来,在项目 src 目录下创建一个名为 database.js 的文件,并将 students 数组添加到该文件中,如下所示:
🔷 第 4 步 — 定义 GraphQL 模式
模式(schema)是一组类型定义(即 typeDefs)的集合,它们共同定义了可以针对 API 执行的查询的结构。这将把 GraphQL 模式字符串转换为 Apollo 期望的格式。创建一个src目录,并在其中创建schema.js文件。
$ mkdir src
$ touch src/schema.js
现在将以下代码添加到文件中:
//* node-graphql-lesson-04/src/schema.js
const { gql } = require("apollo-server")
const typeDefs = gql `
type Student {
id: ID!
email: String!
fullName: String!
dept: Department!
enrolled: Boolean
updatedAt: String
createdAt: String
}
type Department {
id: ID!
name: String!
description: String
students: [Student]
courses: [Course]
updatedAt: String
createdAt: String
}
type Teacher {
id: ID!
email: String!
fullName: String!
courses: [Course]
type: TeacherType
updatedAt: String
createdAt: String
}
type Course {
id: ID!
code: String!
title: String!
description: String
teacher: Teacher
dept: Department
updatedAt: String
createdAt: String
}
input TeacherCreateInput {
email: String!
fullName: String!
courses: [CourseCreateWithoutTeacherInput!]
}
input CourseCreateWithoutTeacherInput {
code: String!
title: String!
description: String
}
type Query {
enrollment: [Student!]
students: [Student!]
student(id: ID!): Student
departments: [Department!]!
department(id: ID!): Department
courses: [Course!]!
course(id: ID!): Course
teachers: [Teacher!]!
teacher(id: ID!): Teacher
}
type Mutation {
registerStudent(email: String!, fullName: String!, deptId: Int!): Student!
enroll(id: ID!): Student
createTeacher(data: TeacherCreateInput!): Teacher!
createCourse(code: String!, title: String!, teacherEmail: String): Course!
createDepartment(name: String!, description: String): Department!
}
enum TeacherType {
FULLTIME
PARTTIME
}
`
module.exports = {
typeDefs,
}
在更新后的代码中,您对 GraphQL schema 进行了以下更改:
- Teacher类型,返回一个Course数组。
- Department类型,返回一个Student数组。
- 具有教师类型的课程类型
- 将“部门”类型的字段转换为“学生”类型。
-
createTeacher mutation 需要 TeacherCreateInput 作为其输入类型。
-
CourseCreateWithoutTeacherInput 输入类型用于 TeacherCreateInput 输入,以便在 createTeacher mutation 中创建教师。
-
createCourse mutation 的 teacherEmail 可选参数。
模式创建完成后,接下来需要创建与该模式匹配的解析器。
🔷 第 5 步 — 定义 GraphQL 解析器
在src目录下创建一个名为resolvers 的子目录。 然后在resolvers子目录下创建三个文件:index.js、query.js和mutation.js ,内容如下:
$ mkdir src/resolvers
$ touch src/resolvers/index.js
$ touch src/resolvers/query.js
$ touch src/resolvers/mutation.js
在 mutation.js 文件中,输入以下内容:
//* node-graphql-lesson-04/src/resolvers/mutation.js
const { prisma } = require("../database.js");
const Mutation = {
registerStudent: (parent, args) => {
return prisma.student.create({
data: {
email: args.email,
fullName: args.fullName,
dept: args.deptId && {
connect: { id: args.deptId },
},
},
});
},
enroll: (parent, args) => {
return prisma.student.update({
where: { id: Number(args.id) },
data: {
enrolled: true,
},
});
},
createTeacher: (parent, args) => {
return prisma.teacher.create({
data: {
email: args.data.email,
fullName: args.data.fullName,
courses: {
create: args.data.courses,
},
},
});
},
createCourse: (parent, args) => {
console.log(parent, args)
return prisma.course.create({
data: {
code: args.code,
title: args.title,
teacher: args.teacherEmail && {
connect: { email: args.teacherEmail },
},
},
});
},
createDepartment: (parent, args) => {
return prisma.department.create({
data: {
name: args.name,
description: args.description,
},
});
},
};
module.exports = {
Mutation,
}
在 query.js 文件中,输入以下内容:
//* node-graphql-lesson-04/src/resolvers/query.js
const { prisma } = require("../database.js");
const Query = {
enrollment: (parent, args) => {
return prisma.student.findMany({
where: { enrolled: true },
});
},
student: (parent, args) => {
return prisma.student.findFirst({
where: { id: Number(args.id) },
});
},
students: (parent, args) => {
return prisma.student.findMany({});
},
departments: (parent, args) => {
return prisma.department.findMany({});
},
department: (parent, args) => {
return prisma.department.findFirst({
where: { id: Number(args.id) },
});
},
courses: (parent, args) => {
return prisma.course.findMany({});
},
course: (parent, args) => {
return prisma.course.findFirst({
where: { id: Number(args.id) },
});
},
teachers: (parent, args) => {
return prisma.teacher.findMany({});
},
teacher: (parent, args) => {
return prisma.teacher.findFirst({
where: { id: Number(args.id) },
});
},
};
module.exports = {
Query,
}
最后,在 index.js 文件中,输入以下内容:
//* node-graphql-lesson-04/src/resolvers/index.js
const { prisma } = require("../database.js");
const { Query } = require("./query.js");
const { Mutation } = require("./mutation.js");
const Student = {
id: (parent, args, context, info) => parent.id,
email: (parent) => parent.email,
fullName: (parent) => parent.fullName,
enrolled: (parent) => parent.enrolled,
dept: (parent, args) => {
return prisma.department.findFirst({
where: { id: parent.dept },
});
},
};
const Department = {
id: (parent) => parent.id,
name: (parent) => parent.name,
description: (parent) => parent.description,
students: (parent, args) => {
return prisma.department.findUnique({
where: { id: parent.id },
}).students();
},
courses: (parent, args) => {
return prisma.department.findUnique({
where: { id: parent.id },
}).courses();
},
};
const Teacher = {
id: (parent) => parent.id,
email: (parent) => parent.email,
fullName: (parent) => parent.fullName,
courses: (parent, args) => {
return prisma.teacher.findUnique({
where: { id: parent.id },
}).courses();
},
};
const Course = {
id: (parent) => parent.id,
code: (parent) => parent.code,
title: (parent) => parent.title,
description: (parent) => parent.description,
teacher: (parent, args) => {
return prisma.course.findUnique({
where: { id: parent.id },
}).teacher();
},
dept: (parent, args) => {
return prisma.course.findUnique({
where: { id: parent.id },
}).dept();
},
};
const resolvers = {
Student,
Department,
Teacher,
Course,
Query,
Mutation,
};
module.exports = {
resolvers,
};
让我们来详细分析一下解析器的变化:
-
createCourse 变更解析器现在使用 teacherEmail 参数(如果传递)在创建的课程和现有教师之间建立关系。
-
新的 createTeacher 变更解析器使用嵌套写入创建教师和相关课程。
-
Teacher.courses 和 Post.teacher 解析器定义了在查询 Teacher 或 Post 时如何解析 courses 和 teacher 字段。它们使用 Prisma 的 Fluent API 来获取关联关系。
🔷 第 6 步 — 创建 GraphQL 服务器
在此步骤中,您将使用 Apollo Server 创建 GraphQL 服务器并将其绑定到端口,以便服务器可以接受连接。
首先,运行以下命令为服务器创建文件:
$ touch src/index.js
现在将以下代码添加到文件中:
//* node-graphql-lesson-04/src/index.js
const { ApolloServer } = require('apollo-server')
const { typeDefs } = require('./schema')
const { resolvers } = require('./resolvers')
const port = process.env.PORT || 9090;
const server = new ApolloServer({ resolvers, typeDefs });
server.listen({ port }, () => console.log(`Server runs at: http://localhost:${port}`));
启动服务器以测试 GraphQL API:
$ npm start
$ npm install nodemon -D
最后,你的 package.json 文件看起来像这样:
{
"name": "node-graphql-lesson-04",
"version": "1.0.0",
"description": "Graphql backend with node, prisma, postgres and docker",
"main": "index.js",
"scripts": {
"start": "nodemon src/"
},
"keywords": [
"Graphql",
"Backend",
"Prisma",
"Postgre",
"Docker",
"Node.js"
],
"author": "Nditah Sam <nditah@telixia.com>",
"license": "ISC",
"dependencies": {
"@prisma/client": "^2.29.1",
"apollo-server": "^3.1.2",
"graphql": "^15.5.1"
},
"devDependencies": {
"nodemon": "^2.0.12",
"prisma": "^2.29.1"
}
}
🔷 第 7 步 — 测试和部署
通过执行以下 GraphQL 查询和变更来测试 node-graphql-prisma 后端:
创建部门
mutation {
createDepartment(name: "Backend Engineering", description: "Express, ApolloServer, Prisma, Docker, Postgres") {
id
name
description
}
}
mutation {
createDepartment(name: "Frontend Development", description: "React, Angular, Vue, Gatsby, CSS, Bootstrap") {
id
name
description
}
}
### 创建课程
mutation CreateCourseMutation($createCourseCode: String!, $createCourseTitle: String!) {
createCourse(code: $createCourseCode, title: $createCourseTitle) {
id
code
title
description
teacher {
id
fullName
}
}
}
### 创建教师
mutation CreateTeacherMutation($createTeacherData: TeacherCreateInput!) {
createTeacher(data: $createTeacherData) {
id
fullName
createdAt
courses {
id
code
title
}
}
}
请注意,只要查询的返回值是 Course,您就可以获取教师信息。在本例中,将调用 Course.teacher 解析器。
最后,提交更改并推送以部署 API:
$ git add .
$ git commit -m "Feature: Add Teacher, Couse, Department"
$ git push
您已使用 Prisma Migrate 成功迁移了数据库模式,并在 GraphQL API 中公开了新模型。
该项目的 GitHub 代码库位于此处。
结论
尽管本课的目的并非比较 REST 和 GraphQL,但仍需强调以下几点:
🔷 虽然 GraphQL 简化了数据消费,但由于其缓存特性、安全性、完善的工具社区和卓越的可靠性,REST 设计标准仍然受到许多行业的青睐。正因如此,加上 REST 的良好口碑,许多 Web 服务都倾向于采用 REST 设计。
🔷 无论选择哪种方案,后端开发人员都必须准确理解前端用户将如何与 API 交互,才能做出正确的设计选择。虽然某些 API 风格比其他风格更容易上手,但只要有完善的文档和演练,后端工程师就能构建出高质量的 API 平台,让前端开发人员无论使用哪种风格都能满意。
延伸阅读
[1] Prisma Fluent-Api
[2] Prisma 组件
[3] GraphQL 入门
[4] Apollo Server 入门
阅读与编程愉快!