一、Prisma概述
(一)Prisma是什么?
Prisma是一个开源ORM框架,包含三部分:
- Prisma Client: 查询构建器
- Prisma Migrate: 数据迁移工具
- Prisma Studio: 操作数据库的GUI工具
(二)Prisma如何工作?
1. 安装
首先要安装Prisma两个依赖模块:
npm init
npm install prisma -S
npm install @prisma/client -S
第一个模块是CLI命令,通过它调用Prisma的各种功能,包括数据迁移、构建client等。
第二个模块是生成@prisma/client初始模块,该模块是数据库与程序之间的代理类,程序使用该模块操作数据库表。当数据库描述文件(Prisma Schema)发生改变时,需要通过npx prisma generate
命令更新该模块。
2. 数据库描述文件:Prisma schema
Prisma schema文件允许开发者通过直观的数据建模语言来定义应用模型。
可以通过npx prisma init
命令来生成初始化schema文件。执行该命令后,会在当前目录创建一个.env文件,并生成一个prisma目录,在prisma目录下创建schema.prisma文件。
.env文件中定义了DATABASE_URL环境变量,可针对实际使用的数据库修改该连接字符串。连接字符串的格式为:
"mysql://username:password@localhost:3306/dbname"
schema文件如下,共配置了三样东西:
- 数据源datasource:指定数据库连接
- 生成器generator:指定使用generate命令时如何生成@prisma/client模块
- 数据模型model:与数据库表对应的模型
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
在VS Code中安装Prisma插件,可获得对.prisma文件的格式化显示。
3. 同步数据库
使用migrate命令进行数据库迁移,将schema中的变化实施到数据库中。
$ npx prisma migrate dev --name first_migration
迁移记录会依次保存在prisma/migrations目录下。
二、模型
数据模型定义中可以包含字段、模型间关系、属性和函数。
(一)定义模型
model CommentTag{
// Fields
@@map("comment_tag")
}
类名采用大坨峰形式,但数据库表名通常使用蛇式,例如common_tag。可以使用@@map属性来重命名数据库表名。
模型的每个记录必须是唯一可识别的,因此每个Model必须至少定义以下一个属性(关于字段属性的解释见后文):
- @unique
- @@unique
- @id
- @@id
(二)定义字段
模型的属性被称为字段,包含字段名、字段类型、类型修饰符(选填)、@属性(选填)。
字段类型分类两类:
- 标量类型,基本数据类型
- 模型类型,例如上例中的Post或Comment[],这些字段又被称为关系字段
1. 标量类型
(1) String
MySQL的默认类型映射:varchar(191)
MySQL原生类型 | @属性 |
---|---|
VARCHAR | @db.VarChar(X) |
TEXT | @db.Text |
CHAR | @db.Char(X) |
TINYTEXT | @db.TinyText |
MEDIUMTEXT | @db.MediumText |
LONGTEXT | @db.LongText |
(2) Boolean
MySQL的默认类型映射:TINYINT(1)
(3) Int
MySQL的默认类型映射:INT
MySQL原生类型 | @属性 |
---|---|
INT | @db.Int |
INT UNSIGNED | @db.UnsignedInt |
SMALLINT | @db.SmallInt |
SMALLINT UNSIGNED | @db.UnsignedSmallInt |
MEDIUMINT | @db.MediumInt |
MEDIUMUNSIGNED | @db.UnsignedMediumInt |
TINYINT | @db.TinyInt |
TINYINT UNSIGNED | @db.UnsignedTinyInt |
YEAR | @db.Year |
(4) BigInt
MySQL的默认类型映射:INT
MySQL原生类型 | @属性 |
---|---|
BIGINT | @db.BigInt |
SERIAL | @db.UnsignedBigInt @default(autoincrement()) |
(5) Float
MySQL的默认类型映射:DOUBLE
MySQL原生类型 | @属性 |
---|---|
FLOAT | @db.Float |
DOUBLE | @db.Double |
(6) Decimal
MySQL的默认类型映射:DECIMAL(65,30)
MySQL原生类型 | @属性 |
---|---|
DECIMAL,NUMERIC | @db.Decimal(X,Y) |
(7) DateTime
MySQL的默认类型映射:DATETIME(3)
MySQL原生类型 | @属性 |
---|---|
DATETIME(x) | @db.DateTime(x) |
DATE(x) | @db.Date(x) |
TIME(x) | @db.Time(x) |
TIMESTAMP(x) | @db.Timestamp(x) |
YEAR |
(8) enum枚举类型
如下,定义一个有两个选项的枚举类型:
enum Role{
USER
ADMIN
}
model User{
id Int @id @default(autoincrement())
role Role @default(USER)
}
2. 类型修饰符
- [] 使字段变成列表。标量类型字段若添加列表修饰符,需要数据库原生支持标量列表;模型类型字段添加列表修饰符,表示one2many关系。
- ? 使字段成为选填,否则为必填字段。
3. @属性
@属性会调整字段或模型的行为,部分属性接受参数。字段使用@添加属性,模型使用@@添加属性。
(1) @id
令字段为主键,可以使用@default()进行注解,该值使用函数自动生成ID,可选函数为:
- autoincrement()
- cuid()
- uuid()
例如:
model User{
id Int @id @default(autoincrement())
name String
@@map("user")
}
(2) @@id
定义复合主键。例如:
model User{
id Int @default(autoincrement())
firstName String
lastName String
email String @unique
isAdmin Boolean @default(false)
@@id([id, firstName, lastName])
}
(3) @default
创建字段默认值,默认值可以是静态值,也可以是以下函数之一:
- autoincrement()
- dbgenerated():当使用从数据库自动生成Schema的内省功能时,若Schema尚无法表示原数据库中该字段的默认值,则使用该函数表示。
- cuid()
- uuid()
- now()
(4) @unique
唯一性约束。若一个model上不存在@id或@@id,则必须有一个使用@unique修饰的字段。@unique修饰不能用于关系字段。@unique修饰的字段必须为必填字段(不能使用?修饰)。
(5) @@unique
定义多个唯一性约束。例如:
model User{
firstname Int
lastname Int
id Int
@@unique([firstname, lastname, id])
}
(6) @@index
创建数据库索引。例如:
model Post{
id Int @id @default(autoincrement())
title String
content String?
@@index([title, content])
}
(7) @relation
定义关联关系,对应数据库的外键。@relation有三个参数:
- name 可为匿名参数,表示关系名,在many2many关系中,该名字会用于构建中间表
- fields 当前模型的字段列表
- reference 关联模型的字段列表
Prisma中有三种不同的关系类型,包括One2One、One2Many、Many2Many。具体可参考(三)定义关系中的内容。
(8) @map
将Schema中的字段名映射为数据库中不同的名称。例如,将firstName字段映射到名为first_name的列:
model User{
id Int @id @default(autoincrement())
firstName String @map("first_name")
}
(9) @@map
将Schema中模型名称映射为数据库中不同的表名。例如:
model User{
id Int @id @default(autoincrement())
name String
@@map("users")
}
(10) @updatedAt
自动存储更新的时间。例如:
model Post{
id String @id
updatedAt DateTime @updatedAt
}
(三)定义关系
1. 一对一关系
User和Profile间有一对一关系,例如:
model User{
id Int @id @default(autoincrement())
profile Profile?
}
model Profile{
id Int @id @default(autoincrement())
userId Int
user User @relation(fields:[userId], reference:[id])
}
- 关系标量字段是数据库中外键的直接体现(上例Profile中的userId。
- 在一对一关系中,没有外键的一侧,关系模型字段必须是选填(上例User中的profile)。
- 关系模型字段在底层数据库中不体现,仅用于生成Prisma Client。
- 可自行决定在关系的哪一侧存储外键。
2. 一对多关系
User和Post间有一对多关系,例如:
model User{
id Int @id @default(autoincrement())
posts Post[]
}
model Post{
id Int @id @default(autoincrement())
authorId int?
author User? @relation(fields:[authorId], references:[id])
}
一对多关系中,存在一个不标注@relation的列表侧(上例User的posts),列表可以为空。
3. 多对多关系
多对多关系是通过中间表来实现的,可以显式声明,也可以隐式自动生成。
(1) 显式声明
Category和Post之间有多对多关系,使用显式声明,需要一张中间表记录关系,例如:
model Post{
id Int @id @default(autoincrement())
title String
categories CategoriesOnPosts[]
}
model Category{
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
model CategoriesOnPosts{
postId Int
post Post @relation(fields:[postId], references:[id])
categoryId Int
category Category @relation(fields:[categoryId],references:[id])
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
上例中间表CategoriesOnPosts中定义了额外的字段assignedAt和assignedBy,这两个字段记录建立关联的时间和由谁建立关联。这些额外字段可不存在。
(2) 隐式声明
隐式多对多关系在关系两侧定义类型为列表的关系字段。关系表存在于底层数据库中,但它由Prisma管理,且不体现在schema中。隐式多对多关系会简化Prisma Client API的调用过程。仍然以Post和Category为例:
model Post{
id Int @id @default(autoincrement())
categories Category[]
}
model Category{
id Int @id @default(autoincrement())
posts Post[]
}
隐式多对多关系要求两侧模型都有单一字段@id,且不能使用多字段id,也不能使用@unique代替@id。
三、Prisma Client
本节案例都将基于如下schema:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model ExtendedProfile {
id Int @id @default(autoincrement())
biography String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
profileViews Int @default(0)
role Role @default(USER)
posts Post[]
profile ExtendedProfile?
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(true)
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Json?
views Int @default(0)
likes Int @default(0)
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
enum Role {
USER
ADMIN
}
(一)CRUD
1. create()、createMany()
创建单条记录:create()
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
const user=await prisma.user.create({
data:{
name: 'hello',
email: 'hello@abc.com'
}
})
创建多条记录:createMany()
const createMany = await prisma.user.createMany({
data:[
{name:'Bob', email:'bob@abc.com'},
{name:'Tom', email:'tom@abc.com'},
{name:'Helen', email:'helen@abc.com'}
]
})
2. findUnique()、findMany()、findFirst()
(1) 获取唯一记录:findUnique()
按@id或@unique字段获取唯一记录。
const user1 = await prisma.user.findUnique({
where:{
id: 3
}
})
const user2 = await prisma.user.findUnique({
where:{
email: 'tom@abc.com'
}
})
按@@id或@@unique复合字段获取唯一记录。
若是复合字段,默认字段名遵循fieldname1_fieldname2模式,也可以在定义复合字段时指定复合字段名。
model TimePeriod{
year Int
quater Int
total Decimal
@@id([year,quater])
}
model TimePeriod2{
year Int
quater Int
total Decimal
@@unique([year,quater], name:'timeperiodid')
}
const timePeriod=await prisma.timePeriod.findUnique({
where:{
year_quater:{
year: 2020,
quater: 4
}
}
})
const timePeriod2=await prisma.timePeriod2.findUnique({
where:{
timepreiodid:{
year: 2020,
quater: 4
}
}
})
(2) 获取所有记录:findMany()
const users=await prisma.user.findMany()
(3) 获取与特定条件匹配的第一条记录:findFirst()
findFirst()在幕后调用findMany(),并接受相同的查询选项。
3. update()、upsert()、updateMany()
(1) 更新记录:update()
const user=await prisma.user.update({
where:{id:1},
data:{email:'alice@abc.com'}
})
(2) 更新或创建新记录:upsert()
const user=await prisma.user.upsert({
where:{id:1},
update:{email:'alice@abc.com'},
create:{email:'alice@abc.com'}
})
(3) 更新一批记录:updateMany()
const updateUserCount=await prisma.user.updateMany({
where:{name:"Alice"},
data:{name:"ALICE"}
})
4. delete()、deleteMany()
(1) 根据id或unique字段删除一条记录:delete()
const user=await prisma.user.delete({
where:{id:1},
//返回被删除用户的name和email
select:{email:true, name:true}
})
(2) 删除符合条件的一组记录:deleteMany()
//删除所有user
const deletedUserCount=await prisma.user.deleteMany({})
const deletedUserCount=await prisma.user.deleteMany({
where:{name:'Bob'}
})
(二)字段和关系选择
1. 字段选择:select
select指定返回的结果中包含哪些字段。
const result1=await prisma.user.findUnique({
where:{id:1},
select:{name:true, profileViews:true}
})
//选择关联模型的字段
const result2=await prisma.user.findMany({
select:{
id:true,
name:true,
posts:{
select:{
id:true,
title:true
}
}
}
})
2. 去重选择:select distinct
distinct可与select配合使用,选出某字段不同的记录。
const result=await prisma.user.findMany({
select:{role:true},
distinct:['role']
})
3. 关系选择:include
include指定返回的结果中包含哪些关系。
const users=await prisma.user.findMany({
include:{posts:true, profile:true}
})
(三)过滤条件和运算符
1. equals、not
const result=await prisma.user.findMany({
where:{
name:{
equals: 'Helen'
}
}
})
// 等价于:
const result=await prisma.user.findMany({
where:{
name: 'Helen'
}
})
const result=await prisma.user.findMany({
where:{
name:{
not: 'Helen'
}
}
})
2. in、notIn
const result=await prisma.user.findMany({
where:{
id:{in:[1,3,5,7]}
}
})
const result=await prisma.user.findMany({
where:{
id:{notIn:[1,3,5,7]}
}
})
3. lt、lte、gt、gte
lt:小于
lte:小于等于
gt:大于
gte:大于等于
获取所有点赞数(likes)小于9的博客:
const getPosts=await prisma.post.findMany({
where:{
likes:{
lt:9
}
}
})
4. contains
获取content包含’abc’的博客数量:
//包含
const result=await prisma.post.count({
where:{
content:{contains:'abc'}
}
})
//不包含
const result=await prisma.post.count({
where:{
NOT:{
content:{contains:'abc'}
}
}
})
5. startsWith、endsWith
const result=await prisma.post.findMany({
where:{
title:{startsWith:'hello'}
}
})
6. AND、OR、NOT
const result=await prisma.post.findMany({
where:{
AND:[
{content:{contains:"abc"}},
{published:{equals:false}}
]
}
})
//AND可省略,上例等价于:
const result=await prisma.post.findMany({
where:{
content:{contains:"abc"},
published:{equals:false}
}
})
const result=await prisma.post.findMany({
where:{
published:{equals:false},
OR:[
{title:{contains:'abc'}},
{title:{contains:'def'}}
],
NOT:{title:{contains:'ghi'}}
}
})
(四)关联过滤
1. some、every、none
一对多关系中,以多方为条件过滤出一方。
some表示只要多方存在至少一条符合条件。
every表示多方要全部符合条件。
none表示多方要全部不符合条件。
const result=await prisma.user.findMany({
where:{
post:{
some:{
content:{contains:'abc'}
},
every:{
published:true
}
}
}
})
//所有未关联post的user
const result2=await prisma.user.findMany({
where:{
post:{
none:{}
}
}
})
//所有未发表过post的user
const result3=await prisma.user.findMany({
where:{
post:{
none:{published:true}
}
}
})
2. is、isNot
一对多关系中,以一方为条件过滤出多方。
const result=await prisma.post.findMany({
where:{
user:{
is:{name:"Bob"}
}
}
})
(五)排序
const usersWithPosts=await prisma.user.findMany({
orderBy:[
{role:'desc'},
{name:'asc'}
],
include:{
posts:{
orderBy:{title:'desc'},
select:{title:true}
}
}
})
(六)分页
1. 偏移分页
偏移分页使用skip跳过一定数量的结果,并使用take选择有限的范围。
const results=await prisma.post.findMany({
skip:3,
take:4
})
偏移分页优点是灵活,缺点是性能低。例如,要跳过20000条记录,并获取前10条记录,数据库仍需要遍历前20000条记录。因此,偏移分页适用于小结果集的分页。
2. 基于游标的分页
基于游标的分页使用cursor和take在给定游标之前或之后返回一组记录。
//第一次查询不使用游标
const firstQueryResults=prisma.post.findMany({
take:4,
orderBy:{id:'asc'}
})
//记录当前游标
var myCursor=firstQueryResults[3].id
//第二次查询从游标位置+1处开始
const secondQueryResults=prisma.post.findMany({
take:4,
skip:1,//跳过游标,避免重复
cursor:{id:myCursor},
orderBy:{id:'asc'}
})
//记录当前游标
mycursor=secondQueryResults[3].id
...
基于游标分页,底层SQL并不使用OFFSET,而是查询id大于游标值的所有记录。适用于大记录集的连续滚动分页。局限性是必须按游标排序,且游标必须是唯一的连续列。
(七)聚合、分组、计数
1. 聚合:aggregate()
聚合函数包括:
- 总数: _count
- 均值: _avg
- 求和: _sum
- 最小: _min
- 最大: _max
const minMaxAge=await prisma.user.aggregate({
_count:{_all:true},
_max:{profileViews:true},
_sum:{profileViews:true}
})
2. 分组:groupBy()
分组可用的聚合函数包括:
- 总数: _count
- 均值: _avg
- 求和: _sum
- 最小: _min
- 最大: _max
const groupUsers=await prisma.user.groupBy({
by:['country'],
_sum:{
profileViews:true
}
})
//返回结果:
[
{ country: 'Germany', _sum: { profileViews: 126 } },
{ country: 'Sweden', _sum: { profileViews: 0 } },
]
groupBy支持使用where和having过滤记录。
//where在分组前过滤所有记录
const groupUsers1=await prisma.user.groupBy({
by:['country'],
where:{email:{contains:'abc.com'}},
_sum:{profileViews:true}
})
//having按聚合值过滤整个组
const groupUsers2=await prisma.user.groupBy({
by:['country'],
_sum:{profileViews:true},
having:{
profileViews:{
_avg:{gt:100}
}
}
})
3. 计数:count()
//user总数
const result1=await prisma.user.count()
//user在关联关系约束下的总数
const result2=await prisma.user.count({
where:{
post:{
some:{
published:true
}
}
}
})