Prisma快速上手

一、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: '[email protected]'
    }
})

创建多条记录:createMany()

const createMany = await prisma.user.createMany({
    data:[
        {name:'Bob', email:'[email protected]'},
        {name:'Tom', email:'[email protected]'},
        {name:'Helen', email:'[email protected]'}
    ]
})

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: '[email protected]'
  }
})

按@@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:'[email protected]'}
})

(2) 更新或创建新记录:upsert()

const user=await prisma.user.upsert({
  where:{id:1},
  update:{email:'[email protected]'},
  create:{email:'[email protected]'}
})

(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
      }
    }
  }
})

你可能感兴趣的:(实用技能,node.js)