如何使用Prisma和PostgreSQL构建REST API
简介
Prisma是用于Node.js和TypeScript的开源ORM。它由三个主要工具组成:
- Prisma Client: An auto-generated and type-safe query builder.
- Prisma Migrate: A powerful data modeling and migration system.
- Prisma Studio: A GUI to view and edit data in your database.
这些工具旨在提高应用程序开发者在数据库工作流中的生产力。Prisma的最大优势之一是提供的抽象层级:应用程序开发者在使用Prisma时,无需研究复杂的SQL查询或模式迁移,而是可以更直观地思考他们的数据。
在本教程中,您将使用Prisma和PostgreSQL数据库,在TypeScript中为一个小型博客应用构建一个REST API。您将使用Docker在本地设置您的PostgreSQL数据库,并使用Express实现REST API路由。在教程结束时,您将在您的机器上运行一个本地的Web服务器,可以响应各种HTTP请求并在数据库中读写数据。
先决条件
这个教程假设如下:
- Node.js version 14 or higher installed on your machine. You can use one of the How To Install Node.js and Create a Local Development Environment guides for your OS to set this up.
- Docker installed on your machine (to run the PostgreSQL database). You can install on macOS and Windows via the Docker website, or follow How To Install and User Docker for Linux distributions.
对于这个教程而言,对TypeScript和REST API有一定的基本了解是有帮助的,但并非必需。
第一步 – 创建你的 TypeScript 项目
在这一步中,您将使用npm建立一个纯TypeScript项目。这个项目将成为您在本教程中要构建的REST API的基础。
首先,为您的项目创建一个新目录。
- mkdir my-blog
然后,进入目录并初始化一个空的npm项目。请注意,这里的-y选项意味着您将跳过命令的交互提示。如果要运行提示,请从命令中移除-y选项。
- cd my-blog
- npm init -y
有关这些提示的更多详细信息,请按照《如何使用Node.js模块与npm和package.json》中的第一步操作。
您将会收到类似下述输出的结果,其中包含默认的回应。
Wrote to /…/my-blog/package.json: { “name”: “my-blog”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo \”Error: no test specified\” && exit 1″ }, “keywords”: [], “author”: “”, “license”: “ISC” }
这个命令会创建一个最小化的 package.json 文件,你可以将其作为你的 npm 项目的配置文件使用。现在你可以开始配置 TypeScript 了。
执行以下命令进行纯TypeScript设置:
- npm install typescript ts-node @types/node –save-dev
这会在你的项目中安装三个开发依赖的包。
- typescript: The TypeScript toolchain.
- ts-node: A package to run TypeScript applications without prior compilation to JavaScript.
- @types/node: The TypeScript type definitions for Node.js.
最后要做的事情就是添加一个tsconfig.json文件,以确保TypeScript对你要构建的应用程序进行正确配置。
首先,运行以下命令创建文件:
- nano tsconfig.json
将以下JSON代码添加到文件中。
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
保存并退出文件。
这个设置是一个TypeScript项目的标准且最小化的配置。如果你想了解配置文件的各个属性,你可以参考TypeScript文档。
你已经使用npm搭建了一个普通的TypeScript项目。接下来,你将使用Docker搭建你的PostgreSQL数据库,并将Prisma与其连接起来。
第二步 — 使用PostgreSQL设置Prisma
在这一步中,您将安装Prisma CLI,创建您的初始Prisma模式文件,并使用Docker设置PostgreSQL并将其连接到Prisma。Prisma模式是您的Prisma设置的主要配置文件,包含您的数据库模式。
从以下命令开始安装Prisma CLI:
- npm install prisma –save-dev
建议将Prisma CLI本地安装在项目中(而不是全局安装),这是最佳实践。这样做有助于避免版本冲突,尤其是在您的设备上拥有多个Prisma项目的情况下。
接下来,您将使用Docker来设置您的PostgreSQL数据库。使用以下命令创建一个新的Docker Compose文件:
- nano docker-compose.yml
现在将以下代码添加到新创建的文件中:
version: '3.8'
services:
postgres:
image: postgres:10.3
restart: always
environment:
- POSTGRES_USER=sammy
- POSTGRES_PASSWORD=your_password
volumes:
- postgres:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres:
这个Docker Compose文件配置了一个可以通过Docker容器的5432端口访问的PostgreSQL数据库。目前数据库的凭据设置为sammy(用户名)和your_password(密码)。请随意将这些凭据调整为您偏好的用户名和密码。保存并退出文件。
通过这个设置,在下面的命令中启动PostgreSQL数据库服务器。
- docker-compose up -d
这个指令的输出将会与这个相似。
Pulling postgres (postgres:10.3)… 10.3: Pulling from library/postgres f2aa67a397c4: Pull complete 6de83ca23e55: Pull complete . . . Status: Downloaded newer image for postgres:10.3 Creating my-blog_postgres_1 … done
您可以通过以下命令验证数据库服务器是否正在运行:
- docker ps
这个命令将会输出类似于这样的内容。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8547f8e007ba postgres:10.3 “docker-entrypoint.s…” 3 seconds ago Up 2 seconds 0.0.0.0:5432->5432/tcp my-blog_postgres_1
当数据库服务器正在运行时,您现在可以创建您的Prisma设置。从Prisma CLI运行以下命令。
- npx prisma init
这个命令将会打印以下的输出结果:
✔ Your Prisma schema was created at prisma/schema.prisma. You can now open it in your favorite editor.
作为最佳实践,你应该在所有Prisma CLI的调用前使用npx,以确保使用本地安装。
执行该命令后,Prisma CLI 会在您的项目中创建一个名为 prisma 的新文件夹。在其中,您将找到一个名为 schema.prisma 的文件,它是您的 Prisma 项目的主要配置文件(包括数据模型)。此命令还会将一个名为 .env 的 dotenv 文件添加到您的根文件夹中,您可以在其中定义数据库连接 URL。
为了确保Prisma了解你的数据库位置,打开.env文件,并调整DATABASE_URL环境变量。
首先,打开.env文件。
- nano .env
现在您可以按照以下方式更新环境变量。
DATABASE_URL="postgresql://sammy:your_password@localhost:5432/my-blog?schema=public"
请确保将数据库凭据更改为Docker Compose文件中指定的凭据。要了解有关连接URL格式的更多信息,请访问Prisma文档。
完成后保存并退出文件。
在这一步中,您使用Docker设置了您的PostgreSQL数据库,安装了Prisma CLI,并通过环境变量将Prisma连接到数据库。在下一部分中,您将定义数据模型并创建数据库表。
第三步 – 定义数据模型并创建数据库表格
在这个步骤中,您将在Prisma模式文件中定义数据模型。然后,使用Prisma Migrate将该数据模型映射到数据库,它将生成并发送SQL语句来创建与数据模型相对应的表格。由于您正在构建一个博客应用程序,该应用程序的主要实体将是用户和帖子。
Prisma利用自己的数据建模语言来定义您应用数据的结构。
首先,使用以下命令打开你的schema.prisma文件:
- nano prisma/schema.prisma
现在,在底部的生成器客户端块之后,添加以下模型定义。
. . .
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
你正在定义两个模型:User(用户)和Post(帖子)。每个模型都有一些字段,代表模型的属性。这些模型将映射到数据库表,而字段则代表各个列。
在User和Post模型中,通过posts和author关联字段之间存在一对多的关系。这意味着一个用户可以与多篇帖子相关联。
保存并退出文件。
有了这些模型,您现在可以使用Prisma Migrate在数据库中创建相应的表格。在您的终端中运行以下命令:
- npx prisma migrate dev –name init
这个命令在你的文件系统上创建一个新的 SQL 迁移,并将其发送到数据库。命令中提供的 –name init 选项指定了迁移的名称,并将用于命名在文件系统上创建的迁移文件夹。
这个命令的输出将类似于这样:
Environment variables loaded from .env Prisma schema loaded from prisma/schema.prisma Datasource “db”: PostgreSQL database “my-blog”, schema “public” at “localhost:5432” PostgreSQL database my-blog created at localhost:5432 The following migration(s) have been created and applied from new schema changes: migrations/ └─ 20201209084626_init/ └─ migration.sql Running generate… (Use –skip-generate to skip the generators) ✔ Generated Prisma Client (2.13.0) to ./node_modules/@prisma/client in 75ms
在prisma/migrations/20201209084626_init/migration.sql目录中的SQL迁移文件中,对数据库执行了以下语句(文件名中的突出部分可能因您的设置而不同):
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL,
"email" TEXT NOT NULL,
"name" TEXT,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Post" ADD FOREIGN KEY("authorId")REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
如果您将 –create-only 选项添加到 prisma migrate dev 命令中,您还可以自定义生成的 SQL 迁移文件;例如,您可以设置触发器或使用底层数据库的其他功能。
在这一步中,你将在Prisma架构中定义数据模型,并使用Prisma Migrate创建相应的数据库表。在下一步中,你将在项目中安装Prisma Client,以便可以对数据库进行查询。
第四步 — 在一个简单脚本中探索Prisma客户端查询。
Prisma Client是一个自动生成的、类型安全的查询构建器,您可以使用它在Node.js或TypeScript应用程序中以编程方式读取和写入数据库中的数据。您将在REST API路由中使用它来访问数据库,取代传统的ORM、普通SQL查询、自定义数据访问层或任何其他与数据库通信的方法。
在这一步中,您将安装Prisma Client,并熟悉您可以使用它发送的查询。在下一步中实现REST API的路由之前,您首先将在一个简单的可执行脚本中探索一些Prisma Client的查询。
首先,使用Prisma Client npm包将Prisma Client安装在您的项目文件夹中。
- npm install @prisma/client
接下来,新建一个名为src的目录,用于存放你的源代码文件。
- mkdir src
现在在新目录中创建一个TypeScript文件。
- nano src/index.ts
所有Prisma Client查询都返回可以在代码中等待的promise。这要求你将查询发送到一个异步函数中。
在src/index.ts文件中,使用一个在你的脚本中执行的异步函数,并添加以下样板代码。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// ... your Prisma Client queries will go here
}
main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect())
以下是对模板的快速解读:
-
- 你从先前安装的 @prisma/client npm 包中导入 PrismaClient 的构造函数。
-
- 通过调用构造函数并获取一个名为 prisma 的实例来实例化 PrismaClient。
-
- 你定义了一个名为 main 的异步函数,在其中添加你的 Prisma Client 查询。
- 你调用 main 函数,捕获任何潜在的异常,并确保 Prisma Client 使用 prisma.$disconnect() 关闭任何打开的数据库连接。
在主函数完成后,你可以开始向脚本中添加Prisma客户端查询。调整index.ts,包括异步函数中的突出显示的行。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const newUser = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@prisma.io',
posts: {
create: {
title: 'Hello World',
},
},
},
})
console.log('Created new user: ', newUser)
const allUsers = await prisma.user.findMany({
include: { posts: true },
})
console.log('All users: ')
console.dir(allUsers, { depth: null })
}
main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect())
在这段代码中,你使用了两个Prisma Client查询。
- create: Creates a new User record. You use a nested write query to create both a User and Post record in the same query.
- findMany: Reads all existing User records from the database. You provide the include option that additionally loads the related Post records for each User record.
保存并关闭文件。
现在使用以下命令运行脚本:开始执行脚本。
- npx ts-node src/index.ts
你将在终端上收到以下输出:
Created new user: { id: 1, email: ‘alice@prisma.io’, name: ‘Alice’ } [ { id: 1, email: ‘alice@prisma.io’, name: ‘Alice’, posts: [ { id: 1, title: ‘Hello World’, content: null, published: false, authorId: 1 } ] }
Note
现在你已经使用 Prisma Client 在你的数据库中读写数据了。在剩下的步骤中,你将实现一个简单的 REST API 的路由。
步骤5——实施您的第一个REST API路径
在这一步中,您将在应用程序中安装Express。Express是用于Node.js的流行Web框架,您将在此项目中使用它来实现REST API路由。您将首先实现的路由将允许您使用GET请求从API获取所有用户。用户数据将使用Prisma Client从数据库检索。
用以下命令安装Express:
- npm install express
由于您正在使用TypeScript,您还需要安装相应的类型作为开发依赖项。运行以下命令来完成这个操作:
- npm install @types/express –save-dev
安装好依赖项后,您可以设置您的Express应用程序。
打开你的主源文件。
- nano src/index.ts
现在删除index.ts中的所有代码,并用以下代码替代,开始你的REST API。
import { PrismaClient } from '@prisma/client'
import express from 'express'
const prisma = new PrismaClient()
const app = express()
app.use(express.json())
// ... your REST API routes will go here
app.listen(3000, () =>
console.log('REST API server ready at: http://localhost:3000'),
)
以下是代码的简要解释:
-
- 你从各自的npm包中导入PrismaClient和express。
-
- 你通过调用构造函数实例化PrismaClient并获取一个名为prisma的实例。
-
- 你调用express()来创建你的Express应用程序。
-
- 你添加express.json()中间件以确保Express可以正确处理JSON数据。
- 你在端口3000上启动服务器。
现在你可以实施你的第一个路由了。在app.use和app.listen之间的语句中,添加这些高亮的行来创建一个app.get调用。
. . .
app.use(express.json())
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany()
res.json(users)
})
app.listen(3000, () =>
console.log('REST API server ready at: http://localhost:3000'),
)
添加完后,保存并退出文件。然后使用如下命令启动本地网页服务器。
- npx ts-node src/index.ts
您将收到以下输出结果:
REST API server ready at: http://localhost:3000
要访问“/users”路径,您可以将浏览器指向http://localhost:3000/users或任何其他HTTP客户端。
在本教程中,您将使用基于终端的HTTP客户端curl测试所有的REST API路由。
Note
为了测试你的路由,打开一个新的终端窗口或标签(以便你的本地Web服务器能继续运行),并执行以下命令。
- curl http://localhost:3000/users
您将收到在上一步中创建的用户数据。
[{“id”:1,”email”:”alice@prisma.io”,”name”:”Alice”}]
这次没有包含帖子数组,因为在实施“/users”路由中没有传递包含选项给findMany调用。
你已经在 /users 路径下实现了你的第一个 REST API 路由。在接下来的步骤中,你将实现剩下的 REST API 路由,为你的 API 添加更多功能。
步骤6 — 实现剩余的REST API路由
在这一步中,您将为您的博客应用程序实现其余的REST API路由。最终,您的Web服务器将响应各种GET、POST、PUT和DELETE请求。
您将要实施的路线包括以下选项:
HTTP Method | Route | Description |
---|---|---|
GET |
/feed |
Fetches all published posts. |
GET |
/post/:id |
Fetches a specific post by its ID. |
POST |
/user |
Creates a new user. |
POST |
/post |
Creates a new post (as a draft). |
PUT |
/post/publish/:id |
Sets the published field of a post to true . |
DELETE |
post/:id |
Deletes a post by its ID. |
首先,你将要实现剩余的两个GET路由。
你可以通过在键盘上按下CTRL+C来停止服务器。然后,你可以首先打开index.ts文件进行编辑以进行更新。
- nano src/index.ts
接下来,在/app.get users路由的实现后面添加高亮的代码行。
. . .
app.get('/feed', async (req, res) => {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
res.json(posts)
})
app.get(`/post/:id`, async (req, res) => {
const { id } = req.params
const post = await prisma.post.findUnique({
where: { id: Number(id) },
})
res.json(post)
})
app.listen(3000, () =>
console.log('REST API server ready at: http://localhost:3000'),
)
这段代码实现了两个GET请求的API路由。
- /feed: Returns a list of published posts.
- /post/:id: Returns a specific post by its ID.
Prisma Client在两个实现中都被使用。在/feed路径的实现中,使用Prisma Client发送的查询会过滤出所有published列值为true的Post记录。此外,Prisma Client查询还使用include来获取每个返回的post相关的作者信息。在/post/:id路径的实现中,你会传递从URL路径中检索到的ID,以便从数据库中读取特定的Post记录。
保存并退出您的文件。然后使用以下命令重新启动服务器:
- npx ts-node src/index.ts
为了测试/feed路径,你可以在第二个终端会话中使用以下curl命令。
- curl http://localhost:3000/feed
由于尚未发布任何帖子,因此返回的结果为空数组。
[]
要测试`/post/:id`路由,你可以使用以下的curl命令:
- curl http://localhost:3000/post/1
这个命令将返回您最初创建的帖子。
{“id”:1,”title”:”Hello World”,”content”:null,”published”:false,”authorId”:1}
接下来,您将实现两个POST路由。在您的原始终端会话中,通过按下CTRL+C停止服务器,然后打开index.ts以进行编辑。
- nano src/index.ts
在index.ts的三个GET路由的实现之后,添加上述的高亮行代码。
. . .
app.post(`/user`, async (req, res) => {
const result = await prisma.user.create({
data: { ...req.body },
})
res.json(result)
})
app.post(`/post`, async (req, res) => {
const { title, content, authorEmail } = req.body
const result = await prisma.post.create({
data: {
title,
content,
published: false,
author: { connect: { email: authorEmail } },
},
})
res.json(result)
})
app.listen(3000, () =>
console.log('REST API server ready at: http://localhost:3000'),
)
这段代码实现了两个POST请求的API路由。
- /user: Creates a new user in the database.
- /post: Creates a new post in the database.
就像以前一样,在这两个实现中都使用了Prisma Client。在/user路由实现中,你将HTTP请求体中的值传递给Prisma Client的创建查询。
“这个/post路径比较复杂。你不能直接将HTTP请求的请求体数值传入; 相反,你需要手动提取它们以传递给Prisma客户端的查询。因为请求体中的JSON结构与Prisma客户端预期的结构不匹配,所以你需要手动创建预期的结构。”
完成后,保存并退出文件。
使用以下命令重新启动服务器:
- npx ts-node src/index.ts
要通过/user路由创建新用户,您可以使用curl发送以下POST请求:
- curl -X POST -H “Content-Type: application/json” -d ‘{“name”:”Bob”, “email”:”bob@prisma.io”}’ http://localhost:3000/user
这将在数据库中创建一个新用户,并打印以下输出:
{“id”:2,”email”:”bob@prisma.io”,”name”:”Bob”}
要通过/post路由创建新发布,您可以使用curl发送以下POST请求。
- curl -X POST -H “Content-Type: application/json” -d ‘{“title”:”I am Bob”, “authorEmail”:”bob@prisma.io”}’ http://localhost:3000/post
这将在数据库中创建一个新的帖子,并将其与邮箱为bob@prisma.io的用户连接起来。它会打印出以下输出:
{“id”:2,”title”:”I am Bob”,”content”:null,”published”:false,”authorId”:2}
最后,您将要实现PUT和DELETE路由。停止开发服务器,然后使用以下命令打开index.ts。
- nano src/index.ts
随后,在实施了两个POST路由之后,添加上面标记的代码。
. . .
app.put('/post/publish/:id', async (req, res) => {
const { id } = req.params
const post = await prisma.post.update({
where: { id: Number(id) },
data: { published: true },
})
res.json(post)
})
app.delete(`/post/:id`, async (req, res) => {
const { id } = req.params
const post = await prisma.post.delete({
where: { id: Number(id) },
})
res.json(post)
})
app.listen(3000, () =>
console.log('REST API server ready at: http://localhost:3000'),
)
这段代码实现了一个PUT请求和一个DELETE请求的API路由。
- /post/publish/:id (PUT): Publishes a post by its ID.
- /post/:id (DELETE): Deletes a post by its ID.
再次强调,Prisma Client在这两个实现中都被使用。在”/post/publish/:id”路由的实现中,需要从URL中获取待发布的帖子的ID,并传递给Prisma Client的更新查询。而在用于从数据库中删除帖子的”/post/:id”路由实现中,需要从URL中获取帖子的ID,并传递给Prisma Client的删除查询。
保存并退出您的文件。
使用以下方式重新启动服务器:
- npx ts-node src/index.ts
您可以使用以下curl命令测试PUT路由:
- curl -X PUT http://localhost:3000/post/publish/2
这个命令将投稿的ID值设置为2。如果您重新发送/fed请求,这个帖子将会在回应中包含。
最后,您可以使用以下curl命令测试删除路由。
- curl -X DELETE http://localhost:3000/post/1
这个命令会删除ID为1的帖子。为了验证该ID的帖子已被删除,您可以使用以下curl命令重新发送GET请求到/post/1路由。
- curl http://localhost:3000/post/1
在这一步中,您为博客应用程序实现了剩余的REST API路由。API现在可以响应各种GET、POST、PUT和DELETE请求,并实现了在数据库中读取和写入数据的功能。
结论
在这篇文章中,您创建了一个REST API服务器,其中包含多个不同的路由,用于为示例博客应用程序创建、读取、更新和删除用户和发布数据。在API路由内部,您使用Prisma客户端将相应的查询发送到数据库。
以下是中文的翻译:
作为下一步,您可以使用Prisma Migrate实施额外的API路由或扩展数据库模式。请访问Prisma文档,了解Prisma的不同方面,并探索在prisma-examples存储库中使用GraphQL或gRPC API等工具的一些现成示例项目。