跳到主要内容

手把手实践入门Prisma

· 阅读需 24 分钟

概要

欢迎来到 Prisma workshop!

Prisma 是一个现代化的后端数据层抽象。包含数据库 ORM,GUI 和类 CLI。

2021 年 6 月 29-30 号是全球 Prisma Day,中国区 workshop 由我来主持。

本次研讨会主要面向 Prisma 初学者,我们来一步一步操作实践,入门 Prisma。

这是研讨会对应的文字版文档。点击查看更多。

相关资源:

中文文档:https://www.baasapi.com/blog/prisma

英文文档:https://pris.ly/a-practical-introduction-prisma

GitHub 仓库:https://github.com/nikolasburk/prisma-workshop

Prisma 文档:https://www.prisma.io/

前提条件:

  • 确保你的电脑已安装 Nodejs v10.16 以上的版本;
  • 电脑已安装 git;
  • 推荐使用 VS Code 编辑器来编写代码;

无需提前了解 SQL 和 Prisma。

流程

在本研讨会中,我们将一步一步操作使用 Prisma 时的各种流程。

  1. 设置 Prisma 和 SQLite 数据库,并使用 Prisma 进行数据建模和数据库变更。
  2. 学习类型安全的 ORM 库 Prisma Client。我们会测试各种查询,从单纯的 CRUD 到关系查询,再到过滤和分页查询。
  3. 学习使用 Prisma Client 实现 REST API。
  4. 学习使用 Prisma Client 实现 GraphQL API。

时间

2021 年 6 月 29 日:

17:00 入场

17:10 设置 Prisma,数据模型和变更

17:30 学习 Prisma Client

18:10 用 Express 实现 REST API

18:40 用 Apollo Server 实现 GraphQL API

形式

课程分为两部分:

  1. 主持人演示:主持人将通过会议或直播的形式展示,也会提供回放视频,你可以随时观看和提问。
  2. 自己动手:看完课程请尽量自己动手实践一次,加深记忆。

联系

提问或加入 Prisma 中国社区微信群可以扫描下方二维码(vx: k961082967)添加主持人微信,麻烦备注 prisma:

prisma community

课程

一:设置 Prisma

目标

本节的目标是设置 Prisma 项目,熟悉 Prisma 的数据建模语言并执行第一次数据库变更。

设置

首先,使用 VS Code 新建一个项目,用 git 克隆GitHub 仓库,按照 README.md 文件中的说明进行操作。

最近因为网络问题,最好使用以下镜像 clone。

git clone https://gitee.com/baasapi-admin/prisma-workshop.git

cd prisma-workshop

npm install
# 也可以使用cnpm 或 yarn 等自己习惯的包管理器

任务

克隆 repo 并安装 npm 依赖项之后,就可以开始本课的任务了 💪

任务 1:创建第一个 Prisma 模型

Prisma schema(通常为“schema.prisma”)文件是所有 prisma 项目的核心。现在可以看到项目目录中的 Prisma schema 如下所示:

// prisma/schema.prisma

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

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

此处 VS Code 可能会有提示,对.prisma 文件扩展安装插件,可以安装一个 prisma 插件以格式化。

当前 schema 中有两个设置项。

datasource 指定了数据库连接是 SQLite 数据库,以及数据库地址。

generator 指定了等会要生成的 Prisma Client 为 JavaScript 语言。

除此之外,prisma schema 还要有 prisma models 模型,也就是真实数据库表的映射。在本例中,我们将创建第一个 prisma 模型。

首先用下面这些字段创建一个 User 用户 模型并选择合适的数据类型:

  • id:一个自动递增的整数 ID,用于唯一标识数据库中的每个用户

  • name:用户名称,此字段在数据库中可以为空

  • email:用户的邮箱地址,此字段在数据库中应该为必需唯一

填写如下:

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
}
任务 2:执行第一次数据库变更

当我们设置好第一个模型后,就可以用它来设置数据库表了。这里我们运行以下命令,就会利用 prisma migrate 库执行数据库变更:

npx prisma migrate dev --name init

这里的 --name init 就是给这次变更起个名字,利于查阅和修改。

如果上面的命令执行没出问题的话,prisma目录中就会多出两个新内容:

  • 一个migrations目录,用于跟踪每次数据库变更的信息

  • 一个dev.db文件,它是我们的 SQLite 数据库文件

任务 3:使用 Prisma Studio 插入数据库数据

好的,我们刚刚使用 Prisma 创建了一个新数据库,其中包含一个名为“User”的表。在进入下一节之前,我们来使用Prisma Studio插入三条数据库数据。运行以下命令打开 Prisma Studio:

npx prisma studio

打开后,在User表中创建三条记录并保存到数据库中。

Prisma Studio 是数据库的 GUI,可用于查看和编辑数据库中的数据。

二:了解 Prisma Client

目标

本节的目标是熟悉 Prisma Client 的 API,尝试一下查询数据库。我们将学到 CRUD 查询,关系查询,过滤和分页等。并且我们还要再加一个原先的用户模型有关联关系的新模型。

设置

继续上一节的项目,我们可以看到有一个 script.ts 文件,里面有一个 main 函数,每次执行该脚本时就会调用这个函数。

小提示

尽量手打课程的代码,不要复制粘贴

这样记忆更深。

充分利用自动补全

Prisma Client 会自动生成基于模型的数据库 API,这样我们就可以利用 VS Code 提供的自动补全功能来提升开发体验。

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
const result = await prisma. // 当你打出那个 . 时,编辑器就会出现自动补全提示,选择合适的API后按下Tab键就能自动写好了
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.disconnect())

auto

任务

在完成每一小节任务后,都在终端执行 npm run dev 命令来运行脚本查看效果。

任务 1:查询所有用户

开始写代码,第一个用最简单的查询,用 console.log 打印结果查看。

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findMany();
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());

如果你 clone 的是 github 版本,那么这里会报错没有@types/node,执行npm i -D @types/node安装即可。

任务 2:创建一个新用户

因为邮箱字段是必填项,所以我们得填写这个:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.create({
data: {
email: "victor@baasapi.com",
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 3:更新一个已存在的用户

给上一步中的用户更新一个名字:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.update({
where: {
email: "victor@baasapi.com",
},
data: {
name: "Victor",
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 4:添加一个 Post 文章表到数据库中

为了尝试更多数据库操作,我们添加一个新的模型,拥有和其他模型的关联关系。

打开schema.prisma 文件,修改为:

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
  • title 是文章标题
  • content 是文章内容
  • published 是文章是否发布
  • author 和 authorId 决定了文章和用户的关系,这里是可选的,就代表文章不一定需要一个作者用户。同时也要在用户模型加入文章的关系,这样才是双向的。

添加完后我们将变更更新到数据库中:

npx prisma migrate dev --name add-post
任务 5:新建一篇文章
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.post.create({
data: {
title: "Hello World",
content: "First",
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 6:将用户和文章关联起来

现在数据库中有几个用户和一篇文章,它们可以通过 authorId 外键连接起来。

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.post.update({
where: { id: 1 },
data: {
author: {
connect: { email: "victor@baasapi.com" },
},
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 7:根据 ID 查询用户
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findUnique({
where: { email: "victor@baasapi.com" },
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 8:查询时只返回部分列数据
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findMany({
select: {
id: true,
name: true,
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 9:嵌套查询

将关联的数据一起查询:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findUnique({
where: { email: "victor@baasapi.com" },
include: { posts: true },
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 10:同时新建用户和文章
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.create({
data: {
name: "Nikolas",
email: "burk@prisma.io",
posts: {
create: {
title: "A practical introduction to Prisma",
content: "Second",
},
},
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 11:过滤查询名字以“V”开头的用户
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findMany({
where: {
name: {
startsWith: "V",
},
},
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());
任务 12:分页查询
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
const result = await prisma.user.findMany({
skip: 2,
take: 2,
});
console.log(result);
}

main()
.catch((e) => console.error(e))
.finally(async () => await prisma.$disconnect());

下一步

好的,我们已经初步了解了基本的操作,还有更多功能比如说排序、upsert、原生等。如果想进一步学习,请查看文档

三:REST API

目标

本节的目标是使用刚才了解到的 Prisma Client 知识和 Express 框架搭建一个 REST API 服务。

设置

我们继续使用上一节的项目,切换一下分支即可,然后在切换后删除数据库文件并重新安装依赖。

git stash
git checkout rest-api
rm -rf prisma/migrations
rm prisma/dev.db
rm -rf node_modules
npm install

可以看到有新的数据模型,和上一节差不多。

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}

重新创建数据库和表:

npx prisma migrate dev --name init

最后,给数据库中添加一些初始数据,执行脚本prisma/seed.ts

npx ts-node .\prisma\seed.ts

任务

好的,现在我们可以看到项目出现了scr目录和index.ts文件,里面包含了 Express 服务和一些预设好的 HTTP 路由,接下来,我们就来依次实现每个接口。

另外还有一个 test.http文件,可以用 VS Code 插件REST Client 来进行 API 调用测试。

GET /users

查询所有用户:

app.get("/users", async (req, res) => {
const result = await prisma.user.findMany();
res.json(result);
});
POST /signup

新建用户:

app.post(`/signup`, async (req, res) => {
const { name, email } = req.body;

const result = await prisma.user.create({
data: {
name,
email,
},
});

res.json(result);
});
POST /post

新建文章:

app.post(`/post`, async (req, res) => {
const { title, content, authorEmail } = req.body;

const result = await prisma.post.create({
data: {
title,
content,
author: {
connect: {
email: authorEmail,
},
},
},
});

res.json(result);
});
PUT /post/:id/views

文章阅读量加 1:

app.put("/post/:id/views", async (req, res) => {
const { id } = req.params;

const result = await prisma.post.update({
where: {
id: Number(id),
},
data: {
viewCount: {
increment: 1,
},
},
});

res.json(result);
});
PUT /publish/:id

发布文章:

app.put("/publish/:id", async (req, res) => {
const { id } = req.params;

const result = await prisma.post.update({
where: { id: Number(id) },
data: {
published: true,
},
});

res.json(result);
});
GET /user/:id/drafts

查询某个用户的草稿:

app.get("/user/:id/drafts", async (req, res) => {
const { id } = req.params;

const result = await prisma.user
.findUnique({
where: { id: Number(id) },
})
.posts({
where: {
published: false,
},
});

res.json(result);
});
GET /post/:id

查询所有用户:

app.get(`/post/:id`, async (req, res) => {
const { id } = req.params;

const result = await prisma.post.findUnique({
where: { id: Number(id) },
});

res.json(result);
});
GET /feed?searchString=<searchString>&skip=<skip>&take=<take>

获取所有已发布的文章,并根据请求参数控制查询过滤和分页。

app.get("/feed", async (req, res) => {
const { searchString, skip, take } = req.query;

const or = searchString
? {
OR: [
{ title: { contains: searchString as string } },
{ content: { contains: searchString as string } },
],
}
: {};

const result = await prisma.post.findMany({
where: {
published: true,
...or,
},
skip: Number(skip) || undefined,
take: Number(take) || undefined,
});

res.json(result);
});

四:GraphQL API

目标

本节的目标是使用刚才了解到的 Prisma Client 知识和 Apollo Server 框架搭建一个 GraphQL API 服务。

设置

我们继续使用上一节的项目,切换一下分支即可,然后在切换后删除数据库文件并重新安装依赖。

git stash
git checkout graphql-api
rm -rf prisma/migrations
rm prisma/dev.db
rm -rf node_modules
npm install

可以看到数据模型和上一节一样。

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}

重新创建数据库和表:

npx prisma migrate dev --name init

最后,给数据库中添加一些初始数据,执行脚本prisma/seed.ts

npx ts-node .\prisma\seed.ts

任务

好的,项目依然包含scr目录和index.ts文件,里面包含了 Apollo Server 服务和一些预设好的配置,接下来,我们就来依次实现每个接口。另外,我们可以打开http://localhost:4000Apollo 自带的 API 页面,去调式接口。

Query.allUsers: [User!]!

查询所有用户:

allUsers: (_parent, _args, context: Context) => {
return context.prisma.user.findMany()
},

查询语句:

{
allUsers {
id
name
email
posts {
id
title
}
}
}
Query.postById(id: Int!): Post

通过 ID 查询文章:

postById: (_parent, args: { id: number }, context: Context) => {
return context.prisma.post.findUnique({
where: { id: args.id }
})
},

查询语句:

{
postById(id: 1) {
id
title
content
published
viewCount
author {
id
name
email
}
}
}
Query.feed(searchString: String, skip: Int, take: Int): [Post!]!

获取所有已发布的文章,并根据请求参数控制查询过滤和分页:

feed: (
_parent,
args: {
searchString: string | undefined;
skip: number | undefined;
take: number | undefined;
},
context: Context
) => {
const or = args.searchString
? {
OR: [
{ title: { contains: args.searchString as string } },
{ content: { contains: args.searchString as string } },
],
}
: {};

return context.prisma.post.findMany({
where: {
published: true,
...or,
},
skip: Number(args.skip) || undefined,
take: Number(args.take) || undefined,
});
},

查询语句:

{
feed {
id
title
content
published
viewCount
author {
id
name
email
}
}
}
Query.draftsByUser(id: Int!): [Post]

查询某个用户的所有未发布的文章:

draftsByUser: (_parent, args: { id: number }, context: Context) => {
return context.prisma.user.findUnique({
where: { id: args.id }
}).posts({
where: {
published: false
}
})
},

查询语句:

{
draftsByUser(id: 3) {
id
title
content
published
viewCount
author {
id
name
email
}
}
}
Mutation.signupUser(name: String, email: String!): User!

新建用户:

signupUser: (
_parent,
args: { name: string | undefined; email: string },
context: Context
) => {
return context.prisma.user.create({
data: {
name: args.name,
email: args.email
}
})
},

GraphQL 语句:

mutation {
signupUser(name: "Nikolas", email: "burk@prisma.io") {
id
posts {
id
}
}
}
Mutation.createDraft(title: String!, content: String, authorEmail: String): Post

新建文章:

createDraft: (
_parent,
args: { title: string; content: string | undefined; authorEmail: string },
context: Context
) => {
return context.prisma.post.create({
data: {
title: args.title,
content: args.content,
author: {
connect: {
email: args.authorEmail
}
}
}
})
},

查询语句:

mutation {
createDraft(title: "Hello World", authorEmail: "burk@prisma.io") {
id
published
viewCount
author {
id
email
name
}
}
}
Mutation.incrementPostViewCount(id: Int!): Post

文章阅读量加 1:

incrementPostViewCount: (
_parent,
args: { id: number },
context: Context
) => {
return context.prisma.post.update({
where: { id: args.id },
data: {
viewCount: {
increment: 1
}
}
})
},

查询语句:

mutation {
incrementPostViewCount(id: 1) {
id
viewCount
}
}
Mutation.deletePost(id: Int!): Post

删除文章:

deletePost: (_parent, args: { id: number }, context: Context) => {
return context.prisma.post.delete({
where: { id: args.id }
})
},

GraphQL 语句:

mutation {
deletePost(id: 1) {
id
}
}
User.posts: [Post!]!

查询某用户的文章:

User: {
posts: (parent, _args, context: Context) => {
return context.prisma.user.findUnique({
where: { id: parent.id }
}).posts()
},
},
Post.author: User

查询文章的作者:

Post: {
author: (parent, _args, context: Context) => {
return context.prisma.post.findUnique({
where: { id: parent.id }
}).author()
},
},

好的,完成所有接口后,我们启动服务,前往 http://localhost:4000 的 playground 进行测试。


恭喜,我们已经完成了这次实践,都是一些比较基础的功能,大家有问题的可以随时提问。如果想要学习更多,可以前往 Prisma 文档地址(prisma.io)阅读。

Prisma 是一个非常优秀的数据管理抽象,能大大方便后端开发。

我本人在几年前接触 prisma,觉得它的价值应该被更多人知道,所以就翻译了文档,建立了社区,方便国内用户学习。虽然中途由于产品定位变更,prisma 经历了一次较大的改动,流失了很多用户,但是经过这次 workshop,我们可以看到新的 prisma 有了更好的体验,超越了第一代。

Prisma 的很多特性和思想也影响了我后来的创业方向和产品设计。当时 Prisma 1 可以直接将 GraphQL 和数据库对应起来,很多人就在群里问,能不能把 prisma 自动生成的接口直接暴露出来给前端用?大部分的项目都是简单的 CRUD,只要设计好数据模型就能利用自动生成的代码直接使用多方便。

当然因为安全问题这是不可以的,但是我也看到了很多朋友的需求就是方便、快速、简单,那么当时我就想,如果把安全措施加上去会如何?

这个简单的想法种子在当时种下,后面我在做企业 IT 咨询时经历了很多项目,也看了中台,低代码等等流行的趋势,一个做 BaaS 平台的想法就逐渐成熟了起来。

相比很多人的需求和企业场景,prisma 不可避免有很多不足之处,比如说不适用大规模分布式应用和大量数据场景、依然需要后端开发人员去配置学习部署等。所以我和我的团队花了很多时间都在解决这些需求,最终发布了清林云 BaaS。

用户在网页端设计数据模型,设计 API 逻辑,配置安全措施,就可以直接开发出一个应用后端出来。另外也可以将这些组合为一个应用,发布到应用市场中共享,让所有人都可以直接使用,也可以直接使用别人已经做好的、符合自己需求的应用。这样,使用 BaaS 就不再需要开发后端,这不就是很多人的需求吗?

我的第二次创业受益于 Prisma 优秀的理念,所以我依然会抽时间维护社区,这也是我对 Prisma 的谢意和致敬吧,预祝它越走越远!