介绍
Prisma 创造了一种 DSL(Domain Specific Language,领域特定语言)。
具体流程:把表映射成了 DSL 里的 model,然后编译这个 DSL 会生成 prismaClient
的代码,之后就可以调用它的 find、delete、create 等 api 来做 crud 了。
先在项目中,安装一下:
pnpm install prisma --save-dev # 安装到开发依赖
prisma 指令
查看 prisma 的所有指令,使用 npx prisma -h
- init:创建 schema 文件
- generate: 根据 schema 文件生成 client 代码
- db:同步数据库和 schema
- migrate:生成数据表结构更新的 sql 文件
- studio:用于 CRUD 的图形化界面,查询 api 的使用方法
- validate:检查 schema 文件的语法错误(vscode 安装 prisma 插件即可)
- format:格式化 schema 文件(vscode 安装 prisma 插件即可)
- version:版本信息
温馨提示: 针对上面的指令,都可以使用
npx prisma xxx -h
来查看各自的具体用法。
拿 init 来举例: npx prisma init -h
其他的指令情况也都是如此的。
接下来就看看在开发过程中常用的几个指令:
init
初始化指令,会生成 prisma/shcema.prisma
和 .env
文件。
prisma init
prisma init --datasource-provider mysql # 指定连接的数据库
prisma init --url mysql://xxx:yyy@localhost:3306/prisma_test # 连接数据库信息
db
创建完 schema 文件之后,就需要对 schema 进行定义,与数据库进行交互。
继续使用 npx prisma db -h
,你会发现有四个指令
prisma db pull # 拉取数据库的结构,生成 model
prisma db push # 根据 model 生成数据库表(数据库里面的表都会进行删除,在执行该命令之前,一定要先 pull 一下)
prisma db seed # 执行脚本插入初始数据到数据库(少用)
prisma db execute # 执行 SQL 语句
# --file 执行 SQL 文件,-- shcama 读取数据库配置信息
prisma db execute --file prisma/test.sql --schema prisma/schema.prisma
针对 seed 的指令执行,需要在 package.json 中添加 prisma 指令。
{
"script": {},
"prisma": {
"seed": "npx ts-node prisma/seed.ts"
}
}
然后执行 prisma db seed
就可以执行 SQL 语句,初始数据到数据库。
其实也不难发现,也就是使用 ts-node 来执行的 seed.ts 文件。
整体结构就是这样的,
migrate
使用 db pull
或者 db push
使本地 schema 和数据库保持一致后,但后续发生变动时,就需要同步:
- 当数据库发生结构变化,使用 db pull 来更新 schema
- 当 schema 发生变动,使用 db push 来更新数据库表结构
这里会存在一个问题,就是无论是拉取还是推送,只是数据库和 schema 的结构保持一致了,但是会存在问题:
- 其一 @prisma/client 是没有更新的;
- 其二,结构变化了,数据也是没有推送的。
那么 prisma 提供了 migrate 指令来解决这个问题,该指令用于迁移。
这里的迁移指的是表的结构发生了变化。
npx prisma migrate -h
这里的 npx prisma migrate dev --name xxx
是多个指令的集合
prisma generate
: 更新 @prisma/clientprisma db push
: 同步数据库表结构prisma db seed
: 同步表的数据--name xxx
:是用于记录迁移文件命名(日期+xxx)
案例演示:
旧的 model 和数据展示
现在增加一个 sex 字段,int 类型
model user {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
age Int
sex Int @db.TinyInt
}
当执行 npx prisma migrate dev --name add_sex
时
报错的原因,很简单,添加了 sex 字段,更新了 @prisma/client 的代码,但是
明确表示了,缺少 sex 字段,这是必填的。
解决办法,两种方案:
- 方案一:修改 seed.ts 文件,添加 sex 字段,然后执行执行命令
const user = await prisma.user.createMany({
data: [
{
name: "copyer",
age: 23,
sex: 1,
},
],
});
- 方案二:使 sex 变为可选字段,也就是说可以设置为 null
model user {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
age Int
sex Int? @db.TinyInt // [!code ++]
}
以上两种方案,都能解决。
迁移日志文件记录着,执行的 SQL 语句来更新数据库
dev 指令的用法就是这样了。
还有一个 reset 指令
npx prisma migrate reset
- 删除表中所有数据(表中可能存在脏数据)
- 执行所有的日志文件(也就是 SQL 集合)
- 更新 @prisma/client 代码
- 执行 seed.ts 来初始化数据
生产环境还有几个指令,当后续使用 prisma 接触到生产环境时,再更新
generate
生成 prisma client 代码,用于操作数据库。
使用 npx prisma migrate dev
就可以省略这一步
其他指令
- format:格式化(借用 vscode 插件完成: prisma)
- validate:检查(借用 vscode 插件完成: prisma)
- studio:图形化界面(CRUD 时有用)
- version: 版本信息
都是一些辅助指令,了解即可。
prisma schema 语法
在上面的介绍中,已经大致了解了 prisma 指令的用法。无论是什么指令,都是对 prisma/schema.prisma
文件进行操作。
接下来就熟悉 schema 文件里面的语法。
datasource
datasource
关键词
当项目创建之初,我们就会执行 npx prisma init
指令,就会开始配置数据库信息,数据库名,用户,密码等等
// 数据库连接信息配置
datasource db {
provider = "mysql" // 数据库类型
url = env("DATABASE_URL") // 数据库连接信息
}
generator
generator
关键词
generator 是生成的意思,一般也就是指生成 @prisma/client 的最新代码。
@prisma/client 默认生成的路径在:node_modules/@prisma/client,但是可以进行修改,指定 output 属性来进行修改。
generator client {
provider = "prisma-client-js",
output = "../generated/client"
}
除了 @prisma/client, 其实也可以安装一些社区的三方包,生成可视化文档,比如:
- prisma-docs-generator:生成 docs 文档,通过 http-server 来进行启动,对 crud 非常有用
- prisma-json-schema-generator:生成 JSON
先安装
pnpm install prisma-docs-generator prisma-json-schema-generator -D
再使用
generator docs {
provider = "node node_modules/prisma-docs-generator"
output = "../generated/docs"
}
generator json {
provider = "prisma-json-schema-generator"
output = "../generated/json"
}
通过 live-server 来启动 docs 来看看,实际效果。
model
model
关键词
model 就是用来定义数据库模型的,其语法最终为被转化为 SQL 语句,同步到数据库。
// 定义枚举值
enum Status {
AAA
BBB
CCC
}
model Test {
// id int类型 自动增长
id Int @id @default(autoincrement())
// name varchar(50) 长度,唯一性
name String @unique @db.VarChar(50)
// age int类型 重新取名:new_age
age Int @map("new_age")
// 性别,可选
sex Int? @db.TinyInt
// 启用/禁用 布尔类型 默认值为 true
enable Boolean @default(true)
// createTime 时间格式类型 默认值为当前创建时间 now()
createTime DateTime @default(now())
// updateTime 时间格式类型,跟随更新 @upadteAt
updateTime DateTime @updatedAt
// 枚举
status Status @default(AAA)
// 表名test改名为new_test
@@map("new_test")
// 定义索引
@@index([id])
}
很容易看出上面的书写规则:第一列是字段名,第二列是类型,第三列是一些其他信息。
类型:
Int
: 数字类型String
: 字符串Boolean
: 波尔类型DateTime
: 时间类型- 自定义枚举
也可以添加一个 ?
表示该字段是可选的,可以为 null。
针对其他信息的定义,其实大致跟 SQL 语句的关键词是保持一致的
@id
定义主键@default
设置默认值: @default(autoincrement()): 主键自动增长;@default(now()): 默认当前时间; @default(true): 默认值为 true@unique
添加唯一约束@map
重命名字段名称,敏感大小写@@index
定义索引@@map
重命名表名,敏感大小写@db.xxx
更加具体化类型,跟 sql 类型是一致的(其实类型大致就分为三种)
字符串
数字
时间
上面是一张表的定义,也就是一个 model;如果存在多个表,那么需要写多个 model;表与表之间的存在关联,model 之间又是如何编写的呢?
表与表:一对多
// 数学老师
model MathTeacher {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
// 一个数学老师对应多个学生
students Student[]
}
// 学生
model Student {
id Int @id @default(autoincrement())
name String @unique
// 先定义一个关联 mathTeacherId
mathTeacherId Int
// 建立外键
mathTeacher MathTeacher @relation(fields: [mathTeacherId], references: [id])
}
- 定义两个 model,一个数学老师表(model),一个学生表(model),一个数学老师对应多个学生
- 数学老师表里面定义一个 students 字段,其类似为 Student(model),就有点类似 ts 中的 class 类,作为类型
- 学生表里面定义一个字段 mathTeacherId,用于记录对应数学老师的 id,也是用于作为外键
- 还需要定义一个字段 mathTeacher,其类型为 MathTeacher,@relation 用于来进行外键关联,把 mathTeacherId 作为外键字段,关联到 MathTeacher 表的 id 字段
如果想数学老师和学生是一对一的,也就是表与表之间的一对一,只需要再学生表上添加 @unique
即可
model Student {
// 先定义一个关联 mathTeacherId,每个学生关联的数学老师的 id 是唯一的
mathTeacherId Int @unique
// 建立外键
mathTeacher MathTeacher @relation(fields: [mathTeacherId], references: [id])
}
最后就是表与表之间的多对多,多对多就会创建一张中间表
model Article {
id Int @id @default(autoincrement())
title String @unique
content String? @db.LongText
tags Article_Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
articles Article_Tag[]
}
model Article_Tag {
articleId Int
article Article @relation(fields: [articleId], references: [id])
tagId Int
tag Tag @relation(fields: [tagId], references: [id])
// 联合主键
@@id([articleId, tagId])
}
- 在中间表,来进行外键的关联
@relation
- 定义联合主键
schema 语法大致就是这样了,只需要记住这些关键词的意义即可
- @id 定义主键
- @default 定义默认值
- @map 定义字段在数据库中的名字
- @db.xx 定义对应的具体类型
- @updatedAt 定义更新时间的列
- @unique 添加唯一约束
- @relation 定义外键引用
- @@map 定义表在数据库中的名字
- @@index 定义索引
- @@id 定义联合主键
prisma crud
了解了 prisma 指令和 schema 之后,执行 prisma migrate dev 之后,就会生成 @prisma/client 代码,就可以进行 CRUD 的操作了。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient({
log: [
{
emit: "stdout",
level: "query",
},
],
});
格式:prisma + 表名 + 方法
创建数据
createMany
: 创建多条create
: 创建单条
// 创建多条
const user = await prisma.user.createMany({
data: [
{
name: "copyer1",
age: 12,
},
{
name: "copyer2",
age: 22,
},
{
name: "copyer3",
age: 23,
},
],
});
// 创建单条
const user = await prisma.user.create({
data: {
name: "copyer4",
age: 44,
},
});
查询数据
findUnique
:是用来查找唯一的记录的,可以根据主键或者有唯一索引(unique)的列(也就是说 where 语句的查询条件是唯一的)。findUniqueOrThrow
:与 findUnique 使用基本一致,就是没有找到结果,处理的结果不一样,findUnique 返回 null,而 findUniqueOrThrow 会抛出异常findMany
:查询多条数据findFirst
:查询第一条数据(也就是 findMany 的第一条数据)
// findUnique
const user = await prisma.user.findUnique({
where: {
id: 1,
},
// 指定返回的字段
select: {
id: true,
name: true,
},
});
// findMany
const user = await prisma.user.findMany({
where: {
age: 12,
},
// 如果是多条数据,就可以排序处理
orderBy: {
name: "desc",
},
/**
* 数据过滤:从第 2 条开始,获取 3 条数据
* 但是感觉不是分页处理,是查询出所有数据,然后再根据条件截取数据
*/
skip: 2,
take: 3,
});
// findFirst
const user = await prisma.user.findFirst({
where: {
// 模糊搜索
age: {
// 包含枚举
in: [],
// 不包含枚举
notIn: [],
// 不等于
not: "",
// 包含
contains: "",
// 以什么开始
startsWith: "",
// 以什么结尾
endsWith: "",
// greater than: 大于
gt: "",
// greater than equals: 大于或等于
gte: "",
// less than: 小于
lt: "",
// less than equals: 小于或等于
lte: "",
// 等于
equals: "",
},
},
});
注意:函数接受的 options 都是可以进行组合的
更新数据
update
:更新单条数据(筛选条件要确定唯一性,id, unique)udpateMany
:更新多条数据(筛选条件是模糊搜索,再更新)upsert
:update 和 insert 的组合,当传入的 id 有对应记录的时候,会更新,否则,会创建记录
// update
const user = await prisma.user.update({
// 更新条件
where: {
id: 1, // 唯一性
},
// 更新数据
data: { name: "james" },
});
// updateMany
const user = await prisma.user.updateMany({
where: {
// name 包含 copyer 的数据,age 全部改为 34
name: {
contains: "copyer",
},
},
data: { age: 34 },
});
// upsert: 有id更新,没有id新增
const user = await prisma.user.upsert({
where: { id: 11 },
update: { name: "copyer11", age: 12 },
create: {
id: 11,
name: "copyer22",
age: 99,
},
});
删除数据
delete
:删除单条数据deleteMany
:删除多条数据
await prisma.user.delete({
where: { id: 1 },
});
await prisma.user.deleteMany({
where: {
id: {
in: [11, 2],
},
},
});
其他
count
: 统计数量,用法跟 findMany 一样,只不过返回的数量aggregate
:统计相关,指定_count
、_avg
、_sum
、_min
、_max
groupBy
:分组
// aggregate
const res = await prisma.user.aggregate({
where: {
name: {
contains: "copyer",
},
},
_count: {
_all: true,
},
_max: {
age: true,
},
_min: {
age: true,
},
_avg: {
age: true,
},
});
/**
* 返回值的格式
* {
* _count: { _all: 3 },
* _max: { age: 23 },
* _min: { age: 12 },
* _avg: { age: 17 }
* }
*/
// groupBy: 按照 email 分组,过滤出平均年龄大于 2 的分组,计算年龄总和返回
const res = await prisma.user.groupBy({
by: ["email"],
_count: {
_all: true,
},
_sum: {
age: true,
},
// 过滤条件
having: {
age: {
_avg: {
gt: 2,
},
},
},
});
console.log(res);
多表新增
采用 create
方法
await prisma.department.create({
data: {
name: "技术部",
// employees 是在创建表时定义的字段
employees: {
// 联表新增单条
create: [
{
name: "小张",
phone: "13333333333",
},
{
name: "小李",
phone: "13222222222",
},
],
},
},
});
换种形式写法,采用 createMany
的方式
await prisma.department.create({
data: {
name: "技术部",
employees: {
createMany: {
data: [
{
name: "小王",
phone: "13333333333",
},
{
name: "小周",
phone: "13222222222",
},
],
},
},
},
});
多表查询
通过 include
关键词来进行查询
const res1 = await prisma.department.findUnique({
where: {
id: 1,
},
// 包含 employees 信息的全部查询出来,类似与左连接
include: {
employees: true,
},
});
const res2 = await prisma.department.findUnique({
where: {
id: 1,
},
// 根据条件查询,并且指定字段返回
include: {
employees: {
where: {
name: "小张",
},
select: {
name: true,
},
},
},
});
多表更新
采用 update 的方式
// 更新 department 的时候,并插入了一条 employee 的记录
const res1 = await prisma.department.update({
where: {
id: 1,
},
data: {
name: "销售部",
employees: {
create: [
{
name: "小刘",
phone: "13266666666",
},
],
},
},
});
更新部门表的同时,也可以改变关联:update 的时候使用 connect
和它关联
const res1 = await prisma.department.update({
where: {
id: 1,
},
data: {
name: "销售部",
employees: {
connect: [
{
// 原来id为 4 的员工关联着部门id为 2 的数据,
// connect 在更新的时候,就可以改变关联,变成部门id为 1 的数据
id: 4,
},
],
},
},
});
console.log(res1);
connectOrCreate
:没有就新增,有就更新。但是我感觉这种写法,基本只有更新,因为新增的时候,根本就拿不到 id
const res1 = await prisma.department.update({
where: {
id: 1,
},
data: {
name: "销售部",
employees: {
// 当 id 为 6 的时候,没有数据,就新增,有数据就更新
connectOrCreate: {
where: {
id: 6,
},
create: {
id: 6,
name: "小张",
phone: "13256665555",
},
},
},
},
});
多表删除
deleteMany
方法进行删除
// 删除部门 id 为 1 的所有员工
await prisma.employee.deleteMany({
where: {
// 关联删除
department: {
id: 1,
},
},
});
prisma 执行 sql
当上面的一系列方法不满足时,就自己写 sql 执行,其格式prisma.$queryRaw
await prisma.$queryRaw`select * from Department`;
方法还是比较多个,借助 prisma-docs-generator 生成 docs 文档,可以快速查找其中的用法。