nest接入prisma,以及prisma使用介绍

814 阅读29分钟

前言

prisma 由于 next 而火,所有肯定有些人为了方便也要集成到 nestjs 中,毕竟 nestjs 更适合写后台,除非是只有一个相对简单的前端操作页面,那倒是一个 next + prisma 解决了,要是用到多端,或者有更多追求,接入到 nest 则是一个不错的选择

prisma中文文档prisma官方文档

prisma-import prisma数据库文件合并工具nestjsdto生成工具(这两个不是必要的,一个是多人开发使用,一个是生成dto的,后续会有所介绍)

ps:个人使用感觉,在 nestjs 中的表现来说, typeorm 比 prisma 更优秀,就从后面的一些介绍应该就能感觉到,毕竟 typeorm 存在时间更久了,举几个例子吧,typeorm 常见的功能

简单对比一下他们优缺点,

  • typeorm 有 设置数据库字段默认 select,有些甚至为了方便还要为了一些关键字分离出另外的表(不然就老老实实要求别人关联时也用select吧,挺痛苦的哈,当然较多优化要求的可能不存在这类问题,不会直接返回所有的值)
  • typeorm 模型监听赋值更优秀,支持异步(prisma 仅仅支持同步)
  • prisma 分页和数量还要使用三方或者自己写扩展
  • prisma 模型的类型不是自己创建的ts文件需要变动时很麻烦
  • nestjs 模块化的概念在 prisma 中被模糊化了,很多人会充分利用prisma全局特性乱搞
  • 由于没有直接生成dto,很多人会使用工具直接一步到位直接使用生成的,这样会偷懒很多,结果是生成的ts类型会循环引用swagger都打不开,需要自己调整(后面介绍方案)
  • prisma由于数据库类型都在 prisma 文件中编写,多人开发成了问题(即使使用prisma-import分离合并也存在其他非便利性问题,编译器也会经常提示出错误,甚至需要重启才正常(使用插件后,不适用插件对于新人基本无法分离开发,一堆报错只能看自己基本功了),后续也会介绍,相比较之下 typeorm 多人开发正确之选
  • 声明的数字只能 bigint|int 实际上很多声明 bigint 类型的数字,实际使用number接收即可满足日常生产,使用了bigint后,会造成生成的数据库返回值类型问题,且会序列化失败,这方面使用一次就了解了,js对于 bigint 的支持可不是太友好(可能计算着就变成 number 了)
  • prisma 相比较 typeorm 要使用更多的工具,想使用数量顺手,他们两个花的时间应该是差不多的,毕竟底层数据库了解才是关键,他们学习使用成本最多一周,有基础的基本上一两天就上手写了
  • 对于其他的什么查询为空时不查询等都是小问题,只能说工具使用不熟练,得练,对于分组合并类的操作,只能说稍微复杂点的统计两个都不好用哈,还是得自己写 sql 语句,会 sql 基础才是王道

测试案例demo

安装环境

配置项目

首先先创建 nestjs 基础项目,有兴趣可以参考我这篇文章

// 全局安装Nest,理解为安装nest环境 
npm i -g @nestjs/cli 
// 使用nest命令创建项目,后面基本就靠他了 
nest new project-name

安装环境 prisma 环境,下面这个是生成我们连接数据库的 .env环境变量 文件,还有我们的 prisma文件夹 编写数据库的文件夹(里面的schemas是分离prisma多人开发用的,需要一些配置支持,后续会介绍)

npx prisma init --datasource-provider mysql 

image.png

安装环境 prisma 使用到的库,prisma 是类型支持,另一个是客户端

yarn add prisma --dev
yarn add @prisma/client

加入常用命令(后续使用工具还会加),放到 package.jsonscripts

//数据迁移指令,会生成数据库迁移文件
"migrate": "npx prisma migrate dev --name init",
//根据scheme生成哦们需要的ts支持文件(一些库加入钩子还能额外生成dto等)
"generate": "prisma generate",
//推送同步数据库
"db:push": "npx prisma db push",
//推送数据库,强制更新(普通情况不推荐),其接受数据丢失,一般测试环境下,走脚本使用该命令,否则无法强推
"db:pushf": "prisma db push --accept-data-loss",

一般我们编写完毕数据库文件后,直接使用 yarn generate,生成我们需要的类型文件,然后使用 yarn db:push 会推送到数据库,即同步数据库

这里基本的配置就差不多了,可以开始了

安装mysql(mac端)

mysql下载地址,我们到这里直接下载 dmg 即可,要是服务器一般为远端 linux, 直接进入服务器,按照别人的步骤来下载配置即可

安装完毕后,在系统偏好找打 mysql,然后启动即可,忘记密码也可以点进去配置,比以前方便太多了

image.png

ps:我自动自动后,基本都是开机自启,基本不用管了(甚至时间久了都会忘了流程,毕竟太简单了😂)

连接数据库

到这里数据库服务我们开启了,但我们还没创建数据库,因此需要创建数据库(nest只会创建表和读写,不能创建数据库)

创建前我们需要下载一些工具来操作和查看数据库,可以已使用 appstore 上面的一些收费的软件,其看起来比较舒服,但需要马内,也可以使用vscode插件等,个人倾向 vscode插件,毕竟免费,丑点无所谓

打开 vscode 搜索 database client,然后下载,下载后我们打开,会出现这个界面,直接输入我们的密码和数据库类型名称即可,数据库填写 mysql,不要填写我们自定义的 database 名字,如下所示填写完成后,点,连接成功

image.png

到这里我们还需要创建我们的数据库 database,因此需要命令,我们新建一个,会出现下面命令,我们在 CREATE DATABASE后面加上我们自定义的 database 名字即可,这里叫 nest_demo,当然也可以创建连接多个数据库

image.png

配置环境变量.env(push数据库)

配置 .env 数据库参数信息,下面的估计一看就明白

//数据库类型://账号:密码@ip:端口号/数据库名字
DATABASE_URL="mysql://root:0123456789@127.0.0.1:3306/nest_prisma_demo"

这里配置好之后,我们编写完毕prisma文件后,直接使用 yarn generate,生成我们需要的类型文件,然后使用 yarn db:push 会推送到数据库,即同步数据库,即可

prisma数据库文件编写

首先 vscode 搜索 prisma,第一个就是,直接导入就行,这样我们的数据库模型文件就有提示了,颜色也正常了

还记得之前 init 生成的 prisma 文件夹么,里面有一个 schema.prisma文件,这就是我们编写数据库关系表的地方

image.png

上图就是那个文件,下面是默认内容,对于另外一个文件夹,是多人开发分离出来的文件,最终通过工具将内容合并到 schema.prisma 文件中去的(个人感觉开发体验一般哈)

image.png

下面就开始编写了,需要注意的是,没有数据库基础,这个搞起来也会有很多问题哈,甚至有些理解起来会有出入,甚至会陷入误区

创建基础表

一个基础表一般包括主键、内容、时间参数,需要不重复的可以加上 @unique()、@@unique([..]),具体更多类型可以去官网查看,默认类型一般看名字就很熟悉是干什么的了

model User {
  //default(autoincrement()) @default(cuid()) //@default(uuid()),可以根据情况使用,一般为了效率使用自增,分布式可以考虑后面两种毕竟有机器码
  id           Int      @id @default(autoincrement()) //Int 4字节,长度有限,10亿多点,实际不需要考虑那么多,当有稍微接近个数字的体系时,性能也需要优化了,需要分表分库了
  name         String
  age          Int
  created_time DateTime @default(now())
  updated_time DateTime @updatedAt
}

一对一

表示一个对一个的关系,一般在副表设置外键,减少主表字段,当然硬是反过来也可以,需要注意的是,外键的一方主键要设置为唯一,否则就是以一对一了

model User {
  id           Int      @id @default(autoincrement())
  ...

  sercet UserSercet?
}

//用户隐私表
model UserSercet {
  id      Int       @id @default(autoincrement())
  user    User      @relation(fields: [user_id], references: [id])
  user_id Int       @unique //一对一这里要强制加上 unique,毕竟重复了就不是一对一了

  password String?
  birth    String?
}

一对多、多对一

表示一种关系,这里一个用户的头像对应一个文件,而文件表则对应着多个用户,这就是一对多,反过来就多对一,外键设置在多的一方,可以明确表示关系

model User {
 id           Int      @id @default(autoincrement())
 ...

 //和file一对多
 head_id Int?
 head    File? @relation(fields: [head_id], references: [id])
}

model File {
 id           Int      @id @default(autoincrement())
 originname   String
 filename     String
 //bigint大数字查询会序列化失败,必要建议字符串,或者使用Int长度不一定够,当然可以换算单位
 size         Int
 //金额类等需要精密或者关键数字可以用 Decimal,代码引入 decimal.js ,计算或者保存数字,这个占用位数略多,需要注意使用位置
 // size         Decimal
 created_time DateTime @default(now())

 //和user多对一
 heads      User[]
}

多对多

这个也比较常见,常见的是显示多对多,隐式多对多,需要注意的是第三张表是多对多关系表,而不是两个表对应的第三张表,关系表表示他们的关系,因此不同的关系对应着不同的关系表

显示多对多则是手动创建一张表,自己维护他们之间的关系,关系表设置好对于两张表的外键,关联好即可,可以通过修改第三张表来更新他们之间关系

隐式多对多则是使用系统封装好的功能,会自动建立第三张表,prisma创建的表是带_开头的,仅仅有两个外键,其他信息都没有,查询时,也不会查询出第三张表信息,而是直接查询到对应关联表的信息(可以说隐藏的好哈),就是修改起来效率看着没那么舒服

显示多对多

model User {
  id           Int      @id @default(autoincrement()) //Int 4字节,长度有限,10亿多点,实际不需要考虑那么多,当有稍微接近个数字的体系时,性能也需要优化了,需要分表分库了
  ...

  //user-article多对多-user表
  collection UserCollectionArticle[]
}

model Article {
  id           String   @id @default(cuid())
  name         String
  created_time DateTime @default(now())
  updated_time DateTime @updatedAt

  //user-article多对多-article表
  collection UserCollectionArticle[]
}

//user-article多对多-收藏关系表-显式多对多
model UserCollectionArticle {
  user       User    @relation(fields: [user_id], references: [id])
  user_id    Int
  article    Article @relation(fields: [article_id], references: [id])
  article_id String

  created_time DateTime @default(now())

  @@id([user_id, article_id]) //复合主键
}

隐式多对多

不需要写 relation,非常简单

//隐式多对多,会自动创建一个只有主键的表,创建直接关联创建即可,更新可以通过update + set来更新他们之间的关系表,
model Company {
  id Int @id @default(autoincrement())
  name     String

  users NewUser[]
}

model NewUser {
  id       Int       @id @default(autoincrement())
  name     String
  companys Company[]
}

一对一加多对多(实践版)

实际关系表中可能还存在,和另外一张表存在一对多 + 多对多的情况,即:自己和关系表一对对一,关系表和另外一张表多对多,因为自己表中和另一一个表有两个关系,在prisma中是不允许重复的(如果允许重复,prisma 反向关联会混合到一起,不过从数据库角度考虑,他们本身应该是两个一对多关系,是可以很容易关联到的,无论是正关联还是反关联)

ps:金额类等需要精密或者关键数字可以用 Decimal,代码引入 decimal.js ,计算或者保存数字,这个占用位数略多,需要注意使用位置


model File {
  id           Int      @id @default(autoincrement())
  originname   String
  filename     String
  //bigint大数字查询会序列化失败,必要建议字符串,或者使用Int长度不一定够,当然可以换算单位
  size         Int
  //金额类等需要精密或者关键数字可以用 Decimal,代码引入 decimal.js ,计算或者保存数字,这个占用位数略多,需要注意使用位置
  // size         Decimal
  created_time DateTime @default(now())

  //和user多对一
  heads      User[]
  shop_cover ShopCoverFile[]
  shop_video ShopVideoFile[]
}

//一对一 + 对对多
model Shop {
  id    Int            @id @default(autoincrement())
  name  String
  cover ShopCoverFile?
  video ShopVideoFile?

  created_time DateTime @default(now())
  updated_time DateTime @updatedAt
}

//shop-user-cover关联,作为cover的关联表,与shop一对一,与File一对多(假设文件可以重复使用关联)
model ShopCoverFile {
  shop    Shop @relation(fields: [shop_id], references: [id])
  shop_id Int  @unique
  file    File @relation(fields: [file_id], references: [id])
  file_id Int

  created_time DateTime @default(now())

  @@id([shop_id, file_id])
}

//shop-user-video关联,作为video的关联表,与shop一对一,与File一对多(假设文件可以重复使用关联)
model ShopVideoFile {
  shop    Shop @relation(fields: [shop_id], references: [id])
  shop_id Int  @unique
  file    File @relation(fields: [file_id], references: [id])
  file_id Int

  created_time DateTime @default(now())

  @@id([shop_id, file_id])
}

自引用关系

自引用关系也是比较常见的(实际上也可以规避),也就是自己关联自己,外键在自己表中,通过自己查询自己,例如:上下级关系等

自引用关系也分为 一对一、一对多、多对多,功能也比较简单,比起基础版本的是,relation 需要给定同一个关系名字,反向关联的也需要这么写(一对一和一对多需要设置外键),其他的就差不多了

// //自引用关系(可以一个表使用多个自引用关系)
// //子引用一对一(上下级),场景还是有一些的
model Level {
  id        Int     @id @default(autoincrement())
  name      String?
  //上一级
  parent_id Int?    @unique
  leader    Level?  @relation("UserGroupRelations", fields: [parent_id], references: [id])
  //下一级(include)
  next      Level?  @relation("UserGroupRelations")
}

//自引用一对多--(组长-组员),场景不多,一般设计基础信息的都会直接使用关系表,明确关系
model LeaderAndMember {
  id        Int               @id @default(autoincrement())
  name      String?
  leader_id Int?
  leader    LeaderAndMember?  @relation("UserGroupRelations", fields: [leader_id], references: [id])
  members   LeaderAndMember[] @relation("UserGroupRelations")
}

//自引用多对多--(互相关注),场景很少,实际一般不会这么用哈,一般一个多对多关系表就解决了,特殊场景使用
model UserFollowUser {
  id         Int              @id @default(autoincrement())
  name       String?
  followedBy UserFollowUser[] @relation("UserFollowRelation")
  following  UserFollowUser[] @relation("UserFollowRelation")
}

字段标量类型(常见的属性类型映射类型)

参考地址,这里只列出常见的一些

以 mysql 为例,这个也是最容易出问题的

- Int      INT 4字节  -- 必要时可以切换其他类型或者字符串(bigint有问题,会序列化失败,不多说)
- Bool     TINYINT(1) -- bool类型
- Float    DOUBLE -- 浮点数
- Decimal  DECIMAL(65,30) -- 高精度数字,例如金额
- DateTime DATETIME(3) -- 时间格式
- String   varchar(191) -- 意味着我们最大存放 191 长度字符,对于一些需求很不友好需要更换

String 比较特殊,也最容易出问题, String 声明几种类型(这个长度根据数据库类型,存放双字节是可能也是这个长度,个人测试如此)

- CHAR           @db.Char(X)	//固定长度,效率较高,但不灵活
- VARCHAR        @db.VarChar(X)	//可变长度,可以自己设置最大长度
- TINYTEXT       @db.TinyText	//可变长度 255 字符, 2^8 - 1
- TEXT           @db.Text	//最多 65535 字符,2^16 - 1
- MEDIUMTEXT     @db.MediumText	//最多 16777215 字符,2^24 - 1
- LONGTEXT       @db.LongText    //最多 4294967295 字符,2^32 - 1

字段修饰符

[] 表示声明一个列表,一般是存在关系的其中一方声明 ? 可选,表示该参数(关系)可能存在、亦可能不存在

属性函数

一般默认值使用的,这里列举几个,前面应该已经介绍到了

@default() //这个应该知道默认值,下面的基本上都是往里面放的
//默认自增,一般作为主键
autoincrement()
//生成cuid全局唯一值
cuid()
//生成 uuid唯一值,和上面的类似都由机器码和随机数组成,看自己选择,分布式一般使用
uuid()
//记录时间,一般create_time 的默认值为这个,表示当前时间,存入零时区时间
now()

//这个也额外提示一下,这个是更新这条数据时,时间会自动更新,但不是所有的更新时间都依赖这个,必要时可以自行赋值(过于依赖这个,可能会出现一些跨时间段的bug)
@updatedAt

prisma数据库操作

创建(插入)

创建时除了使用自动生成id的策略,也可以不设置默认值,自己手动赋值id(这里就不演示了)

也可以在创建的同时创建其关联数据

//创建一条数据,顺道给head_id关联一个文件,不关联undefined或者null即可
await prisma.user.create({
    data: {
        name: '哈哈',
        age: 20,
        head_id: 10,
        // head_id: undefined,
    },
});
//创建多个
await prisma.user.createMany({
    data: [
        {
            name: '哈哈',
            age: 20,
            head_id: 10,
        },
    ],
});
//创建时可以连关联数据一同创建,例如用户数据和其头像信息)
await prisma.user.create({
    data: {
        name: '哈哈',
        age: 20,
        head: {
            create: {
                originname: '文件名字',
                filename: '123.png',
                size: 100,
            },
        },
    },
});

更新

更新不多说,需要使用 update + where + 唯一索引 更新某一条内容,也可以通过 updateMany + 非唯一字段、集合一次将多条数据更新成同一个或者按照某个规则更新(自增自减)

和创建一样,也支持更新自己的同时,支持增、删、改关联表数据

//更新一条
await prisma.user.update({
    where: {
        id: 1,
    },
    data: {
        name: 'ls',
    },
});
//更新多条,只能更新成一样的,否则只能一条一条更新
await prisma.user.updateMany({
    where: {
        age: 16,
    },
    data: {
        age: 18,
    },
});
//年龄小于18的都自增1(原子操作)
await prisma.user.updateMany({
    where: {
        age: {
            lt: 17,
        },
    },
    data: {
        age: {
            increment: 1, //增加,原子操作
            // decrement: 1,//减少,原子操作
        },
    },
});
//更新自己的同时,支持增删改关联表
await prisma.user.update({
    where: {
        id: 1,
    },
    data: {
        name: '哈哈',
        head: {
            create: {
                originname: '文件名字',
                filename: '123.png',
                size: 100,
            },
            update: {
                where: {
                    id: 2,
                },
                data: {
                    filename: '234.png',
                },
            },
            delete: {
                id: 2,
            },
        },
    },
});

创建或者更新(upsert)

适用于某些特殊场景,需要根据一些信息判断是创建还是更新用户,例如:假设名字为唯一值,需要一个名字admin的用户,有就更新没有就创建,并且将密码设置为123456

//更新或者创建,有id就更新,没有就创建
await prisma.user.upsert({
    where: {
        name: 'admin',
    },
    create: {
        name: 'admin',
        password: '123456',
    },
    update: {
        password: '123456',
    },
});

实际还是有更合适方便的场景的,这里就不多描述了,遇到了会用就行

隐式多对多更新

隐式多对多由于不能直接操作第三张表,因此需要使用 set 方法来更新关联数据,每次都相当于重新set新关系

//主要是隐式多对多,显示多对多跟操作正常表一样,只不过显示的多对多表,需要通过删除和创建来断开建立关系
//隐式多对多的创建不多说,可以直接create一起创建,也可以连接已有数据的两者
//主要介绍两者的连接,也就是关系的建立,通过where筛选本组数据,然后通过 update + set 和另外一张表建立关联
await prisma.company.update({
    where: {
        id: 10, //company id 为10的和 user表中 id为 10、11的数据建立关联
    },
    data: {
        users: {
            set: [{ id: 10 }, { id: 11 }], //使用此操作会更新关联数据,以前的关联会被覆盖
        },
    },
});
//如果觉的隐式多对多不好用,对于关联表操作比较频繁,嫌弃效率低,可以采用显式多对多的方式,那么可以直接操控关系
//因此也可以看出,隐式多对多,比较适合关系表比较纯粹,且操作没那么繁琐的情况
//(例如点赞一个视频、文章,即使关联没有其他数据,如果操作很频繁,那么隐式多对多效率确实低了,可以采用显示多对多,当然也可替换其他手段应对)

ps:有些偷懒的操作会将一对一 + 一对多直接写成隐式多对多,这样便捷很多,毕竟多对多是可以包含一对多关系的

delete真删

平时很多数据是有价值的使用假删,或者懒得删关联表使用的假删,但很多场景还是会用到真删的,因此需要用到delete,一定要记得设置 where,这也是新手sql 语句最容易出现的错误

//这个操作平时看别人用的少,不代表其不会用,很多数据有价值不会删是其一,其二是开发偷懒了,懒得删关联表才出现的标识位代替删除
//实际上有不少数据是需要删除的,否则会有冗余错误或影响后续操作等(例如:关系表,带有少量数据的关系表,临时数据表等)
//删除一条,需要使用unique的键值
await prisma.user.delete({
    where: {
        id: 1,
    },
});
//大量删除,支持模糊
await prisma.user.deleteMany({
    where: {
        name: '',
    },
});

查询

基础查询

查询用的也是比较多的,常见的是精确查询、模糊查询、关联查询、分页查询等

//查找唯一,条件为unique
const user = await prisma.user.findUnique({
    where: {
        id: 1,
    },
});
//条件部位unique,可能存在多条,查找一条
await prisma.user.findFirst({
    where: {
        name: '大黄',
    },
});
//查找多条
await prisma.user.findMany({
    where: {
        name: {
            contains: '大黄',
        },
    },
});
//查找关联,排序
await prisma.user.findMany({
    where: {
        name: {
            contains: '大黄',
        },
    },
    //取出关联数据
    include: {
        head: true,
        collection: {
            include: {
                user: true,
                article: true,
            },
        },
    },
    //orderBy排序
    orderBy: {
        created_time: 'desc',
        // updated_time: 'asc',
    },
});
//选择字段查询select
await prisma.user.findMany({
    where: {
        name: {
            contains: '大黄',
        },
    },
    //可以代替include,不同的是,select只会拿出写出的的值,include则是取出所有的非关联,将写出的关联数据关联出来
    //这里只拿用到的数据,可以提升查询效率和节省带宽
    select: {
        id: true,
        name: true,
        head: true,
        collection: {
            include: {
                user: {
                    //user表只拿出我们想要的数据
                    select: {
                        id: true,
                        name: true,
                    },
                },
            },
        },
    },
});
//复合id查询,会生成一个复合键,通过该键值查询
await prisma.shopCoverFile.findUnique({
    where: {
        shop_id_file_id: {
            shop_id: 1,
            file_id: 1,
        },
    },
});
//如果本身就是主键(关联表复合主键),实际上不用复合键值,用这个且查询
await prisma.shopCoverFile.findUnique({
    where: {
        shop_id: 1,
        file_id: 1,
    },
});
//对于unique这类复合主键,则必须使用复合主键功能,才是实现findUnique功能
//否则只能使用findFirst
await prisma.newShop.findUnique({
    where: {
        pre_name: {
            pre: '1',
            name: 'b',
        },
    },
});
//查询条件
await prisma.user.findMany({
    where: {
        //条件默认为且,即为同时满足方可
        id: 10,
        head_id: 20,
        // 和上面等同
        // AND: {
        //   id: 10,
        //   head_id: 20,
        // },
        //条件或,里面的条件满足一条即可,整体和外面的为且的关系
        OR: [
            {
                id: 1,
            },
            {
                name: '哈哈',
            },
        ],
        //取出条件不满足的,数组或者对象
        NOT: {
            id: 2,
        },
        //NOT: [{ id: 2}],
        age: {
            //小于等于大于等于,取值范围的不多说了
            lt: 20,
            lte: 30,
            gt: 40,
            gte: 40,
        },
        name: {
            contains: '包含', //模糊查询, 相当于'%包含%',
            startsWith: '包', //看名字就知道,不需要多介绍了吧
            endsWith: '含',
        },
    },
});

分页查询、查询数量

//分页,列表常用的
const users = await prisma.user.findMany({
    where: {
        name: {
            contains: '李',
        },
    },
    //跳过的数据条数,这里就是获取第21~30条
    skip: 20,
    //取出数量
    take: 10,
});
//获取数量
const count = await prisma.user.count({
    where: {
        name: {
            contains: '李',
        },
    },
    //跳过的数据条数,这里就是获取第21~30条
    skip: 20,
    //取出数量
    take: 10,
});
//公用查询,通过设置类型查询
const options: {
    where: Record<string, unknown>;
    skip: number;
    //取出数量
    take: number;
} = {
    where: {
        name: {
            contains: '李',
        },
    },
    //跳过的数据条数,这里就是获取第21~30条
    skip: 20,
    //取出数量
    take: 10,
};
await prisma.user.findMany(options);
await prisma.user.count(options);
//上面的明显还是不好用,可以直接用扩展的$extends的方式扩展,后面简单介绍一下
//或者直接使用 prisma-extension-pagination 组件,这个组件也挺好用的,代码也不多

游标

游标是分页查询的一种优化手段,缺点是使用游标时必须传递唯一的连续列参数(有序),会从其后面开始查询,意味着必须知道上一页的最后一个,适合用于需要一页一页查询的场景(例如:手机端每次都是拉去下一页)

优点很明显效率高了,缺点是应用场景比较局限,且需要额外传递最后一条id,第一次访问或者跳页的情况下无法使用,且无法获取较为实时的第n页的数据,毕竟是从某个索引之后开始,如果过了一天还这么搞,就有点头疼了(当然是可以通过手段调整的,不过一直往后翻一般不会在意最新数据,或者最新的就在后面哈)

//游标
await prisma.user.findMany({
    where: {
        name: {
            contains: '李',
        },
    },
    cursor: {
        id: 20,
    },
    //使用游标后,就不使用默认的跳过了
    // skip: 20,
    //取出数量
    take: 10,
});

prisma 事务

事务也是数据库开发中密不可分的一环,它主要能够保证我们的多个操作数据后数据的一致性(原子、一致、持久、隔离),这里不多介绍,简而言之,多个密不可分的操作,成功一起成功,失败一起失败,否则容易产生脏数据(如果你的同伴经常产生脏数据,那么可能就是这个原因),此外事务除了会降低效率,用不好会产生死锁,还不好排查,当然看个人关注点,个人认为数据安全应当为首任

prisma事务主要分为两种:普通并发事务(并发无序)、交互式事务(有些操作有顺序)

实际上个人看来,交互式事务就包含普通并发事务了,两者差不多,既然给出了两个那就两个都简单介绍下

普通并发事务,无序,只要两个都成功就行

//普通事务的使用,假设同时更新两个用户信息,要一起更新成功,要不就都不更新
await prisma.user.update({
    where: { id: 1 }
    data: {
        money: { increment: 1 } //加1分钱
    },
})
await prisma.user.update({
    where: { id: 2}
    data: {
        money: { decrement: 1 } //减少1分
    },
})

交互式事务,一般为有序,按照流程整个函数执行成功则成功

//交互式事务的使用,假设同时创建用户和文章,需要先创建用户,然后在创建文章(实际可以用前面的创建关联语句,创建文章的同时创建关联的用户,也差不多是使用的这个)
await prisma.$transaction(async (prisma) => {
    const user = await prisma.user.create({
        data: {
            name: '文章2'
        },
    })
    await prisma.article.create({
        data: {
            name: '文章1'
            author_id: user.id
        }
    })
})

为什么说交互式事务可以包含普通事务,下面的语句看着是不是和普通事务类似了呢,当然别人提供了普通事务的,直接使用即可,不仅代码简单,可能效率还高哈

await prisma.$transaction(async (prisma) => {
    await Promise.all([
        await prisma.user.update({
            where: { id: 1 }
            data: {
                money: { increment: 1 } //加1分钱
            },
        })
        await prisma.user.update({
            where: { id: 2}
            data: {
                money: { decrement: 1 } //减少1分
            },
        })
    ])
})

事务类型问题

里面的事务类型 似乎没有直接导出,并且事务类型和 prisma默认类型 不一样,如果我们有组合功能,事务里面需要调用其他模块功能的话,需要传递 事务prisma,因此我们需要取出我们的事务类型,避免其他模块引用 事务prisma 出现没有提示的问题

ps:使用我们的 infer + typeof ,在通过传入事务调用函数,这样就可以取出我们的事务对象类型了,当然这个要是学明白了,ts 的类型体操,相信很快就可以毕业了

//就卸载这个方法中一起导出就行了
export const prisma = new PrismaClient()


type PrismaTransactionFunctionsType<T> = T extends (
    fn: (prisma: infer P) => Promise<unknown>,
) => Promise<unknown>
    ? P
    : any

export type PrismaTransactionType = PrismaTransactionFunctionsType<
    typeof prisma.$transaction
>

prisma扩展

上面碰到了一个头疼的事情,就是分页的时候一般都要附带总数量或者其他信息,typeorm 默认就有,这个没有,我们可以通过使用第三方 prisma-extension-pagination 来解决

又或者直接使用我们的 extends来解决,毕竟怎么看这个操作都不是很复杂是吧,那么我们就来实现一下吧

通过extends-model实现带数量的分页数据

话不多说直接上代码

//外面的函数是声明 extension类型的,也可以直接一个大括号写到 prisma 的 extends 当中
export default Prisma.defineExtension({
    name: 'prisma-extends-pagination', //设置名字,有错误时知道是哪个错误
    model: {
        $allModels: { //声明为所有model,可以针对单个model
            //声明方法
            async findAndCount<T>(
                this: T, //一看就是我门的model对象,很多类型都是根据这个来的,类型体操要用
                { page, limit, cursor, where, ...query }: PrismaQuery<T> = {}, //接收参数
            ): Promise<PaginationType<T>> {
                //获取查询上下文我们的model,得用他调用查询
                const context = Prisma.getExtensionContext(this) as any;
                //处理分页
                const currentPage = page ? Number(page) : 1;
                const take = limit ? Number(limit) : 10;
                const skip = (currentPage - 1) * take;
                //查询并且调用我们的数量对象
                const res = await context.findMany({
                    ...query,
                    skip: skip > 0 ? skip : 0,
                    take,
                    cursor,
                    where,
                });
                //数量只需要用到where即可
                const count = await context.count({
                    where,
                });
                //返回我们的分页固定数据结构,可以自行调整
                return {
                    items: res,
                    currentPage: currentPage,
                    pageCount: take,
                    totalCount: count,
                    totalPages: Math.ceil(count / take),
                };
            },
        },
    },
});

看到了类型,类型是怎么传递的,还记得上面的 allModalsthis 么,我们通过 this 类型,确定我们调用数据库模型的类型,以此来搞定我们的 wherecursor 游标即可,where 我们直接使用 findFirst,理由就是参数全cursor需要使用索引,需要使用的参数类型 findUnique 这样就设置的差不多了

整体代码如下所示

export type PrismaQuery<T> = {
    where?: Prisma.Args<T, 'findFirst'>['where'];
    page?: number | string; //page从1开始
    limit?: number | string; //默认为10
    cursor?: Prisma.Args<T, 'findUnique'>['where'];
};

export type PaginationType<T> = {
    items: T[];
    currentPage: number;
    pageCount: number;
    totalCount: number;
    totalPages: number;
};

export default Prisma.defineExtension({
    name: 'prisma-extends-pagination',
    model: {
        $allModels: {
            async findAndCount<T>(
                this: T,
                { page, limit, cursor, where, ...query }: PrismaQuery<T> = {},
            ): Promise<PaginationType<T>> {
                const context = Prisma.getExtensionContext(this) as any;
                const currentPage = page ? Number(page) : 1;
                const take = limit ? Number(limit) : 10;
                const skip = (currentPage - 1) * take;
                const res = await context.findMany({
                    ...query,
                    skip: skip > 0 ? skip : 0,
                    take,
                    cursor,
                    where,
                });
                const count = await context.count({
                    where,
                });
                return {
                    items: res,
                    currentPage: currentPage,
                    pageCount: take,
                    totalCount: count,
                    totalPages: Math.ceil(count / take),
                };
            },
        },
    },
});

写完之后,别忘了给 prisma扩展上

//刚才的我写到了同目录的 pagination.ts文件中,导出即可
import pagination from './pagination';

export const prisma = new PrismaClient().$extends(pagination)

调用一下,发现有方法了,我们直接打印

const { page_num, page_size } = { page_num: 1, page_size: 10 };
const res = await prisma.file.findAndCount({
    where: {
        id: 1,
    },
    page: page_num,
    limit: page_size,
});
console.log(res);

打印结果如下,完成了

//返回的查询结果
{
  items: [
    {
      id: 1,
      originname: '测试.png',
      filename: '131312312.png',
      size: 100,
      created_time: 2024-05-28T00:00:00.000Z,
      url: 'http://www.xxx.com/131312312.png'
    }
  ],
  currentPage: 1,
  pageCount: 10,
  totalCount: 1,
  totalPages: 1
}

extends-result实现对数据库模型查询结果的订阅监听(给结果赋值)

这个看着很好,话提前说了,他不支持异步赋值,异步的操作(异步签名别看了),同步的没问题

类似的操作,将上面的 model改成 result 即可,我们假设只针对 file,当其查询时,额外签名塞入一个url参数

//同步签名测试方法
function signUrl(filename: string) {
    //配上自己的签名,不能是异步,必须同步,异步自己想其他办法吧
    return `http://www.xxx.com/${filename}`;
}

export default Prisma.defineExtension({
    name: 'prisma-extends-file-url',
    result: {
        file: { //真多 file 数据库模型
            url: { //需要创建一个 url 的参数,依赖于 needs 中的参数
                needs: { filename: true },
                compute(file) {
                    // the computation logic
                    // return file.filename;
                    // 必须是同步签名方法
                    return signUrl(file.filename); //结果返回的内容会赋值给file对应的参数(url)
                },
            },
        },
    },
});

这样上面就实现了订阅监听,可以的是不支持异步,异步那个参数就是一个 promise,这里是不会await的,没办法

当然如果想写起来偷懒,假设要实现一个签名功能,可以使用扩展,也可以自己封装 utils的方式,直接传递路径,然后自动根据路径解析,优点减少了一点代码,缺点执行效率变得更低了,还得先遍历出来指定对象

ps: extends 挺好用,但也过犹不及,不是所有的功能都要放到里面,我们很多业务都可以在这里面得到扩展,非常方便,并且有些版本的功能可能已经删除了,或者有问题,需要注意,可以看看文档(例如里面写的中间件功能)

prisma的一些有助于开发的工具

prisma-import prisma数据库文件合并工具nestjsdto生成工具

prisma模型多人开发与 prisma-import

之前也看到了,我们写数据库模型的时候都是在 schema.prisma这一个文件中编写,如果是一个人开发没什么问题,如果是多人开发,那么初期建立数据库时,将会有数不尽的冲突(甚至后面需求变动或者有疏漏改动也会冲突),这是非常不好的,因此需要每个人分开编写自己模块的数据模型,那么最后只需要合并到一个文件中即可

这种方式,我之前编写过脚本,用着还行,后面发现有这种 prisma-import 工具直接用了,可以去除 import 并且将文件合并到指定位置,好用的很,直接用就对了

除了一开始我们在 vscode 导入的 prisma 插件,我们再导入一个 prisma-import 插件,这个插件能够识别我们的 import 功能,毕竟我们要关联其他人的数据模型,还是要 import 的,当然写熟练了,import 实际上不用也行,就是会提示报错,不过这没事

有时候即使正常导入,写着也没问题,也会报错,这就是bug了,重启一下 vscode 就会正常,所以个人也是更推荐一开发就完事了,多人还是 typeorm 更好用

预备工作做完了,开始安装

yarn add prisma-import

然后,将执行放入到 scripts 中,需要设置合并源地址导出目的地址

// -f 是强制执行, -s 是源文件目录 -o导出目的目录
"prisma:merge": "prisma-import -f -s ./prisma/schemas/**/*.prisma -o ./prisma/schema.prisma",

相信开始也注意到了 prisma 中的 schemas 文件夹了,里面就是存放我们分离的 prisma 文件了,里面可以再放一层文件夹,也可以直接用,我们起名一个 base.prisma 将我们的 schema.prisma 的基础内容放置进去,这样导出时会自动写到我们的基础文件信息,而外面的 schema.prisma文件删除甚至 git 忽略都可以,这个使用命令导出即可

image.png

我们直接使用命令 prisma:merge 就可以强制合并并导出到目的文件中了,并去除 import(ps:我的脚本没有去除 import 就被我扔了,懒得写了🤣),然后调用 generate 生成数据库类型文件即可

prisma生成dto的工具

这个就用我们上面说的那个插件即可,我搜了几个,发现有些甚至是依赖这个的,有些不是,这个下载量相对比较高,就用它了,并且还挺好用,就用他吧

直接导入

//导入到 dev 因为我们开发模式会生成,直接作为正式代码使用,到正式服务器实际执不执行都行
yarn add @brakebein/prisma-generator-nestjs-dto --dev

这个安装完还没结束,需要在我们的 基础 prisma(schem.prisma、base.prisma) 文件下面加上配置

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["interactiveTransactions"] //4.7版本之前用不了交互事务,要加上,4.7开始就支持了,不需要加
}

datasource db {
  provider = "mysql"a
  url      = env("DATABASE_URL")
}
//看到上面那两个就知道咋回事了吧
//实际上我们一般直接设置 output路径到我们项目 src 指定目录下就行了
generator nestjsDto {
  provider                      = "prisma-generator-nestjs-dto"
  prettier                      = "true"
  exportRelationModifierClasses = "false" //是否包含relation关联,会额外生成带关联的dto(不建议直接用,但是粘贴很方便)
  output                        = "../src/generated-nest-dto" //输出路径
}

到这里就完成了,当我们使用 yarn generate 的时候,除了生成数据库基础类型之外,还会背着个勾住,额外生成我们的 dto 文件到我们的指定目录中

如下所示,就是我们生成的 dto 文件了

image.png

dto 循环引用问题

使用时会发现, entity 是最全的,很多人会直接使用,然后会发现循环引用了,swaggerUI 打开时会递归,因此出现 bug,这个自己写倒是没事,跟别人合作开发要被烦死,毕竟等于没有文档

其循环引用来源就是 entity 关联的还是 entity,关联到基础类后会反关联,直接成环,这才导致循环引用

因此别直接使用 entity,直接使用基础的 dto,会发现少了关联类,我们就另起一个文件,继承自基础 dto,然后将 entity 中的关联内容复制粘贴到我们继承类中(为了复制粘贴方便,数据库模型的关联都写到最后边吧)

这样就形成了两份文件,一份基础dto(工具类生成),一份受我们掌控关联其他dto的dto文件,我们的关联类可以根据自己需要,灵活引用其他类的基础dto和关联dto,一般我们关联到基础类后就直接使用基础dto了,例如:其他表关联到用户一般就完事了,不需要用户关联其他的表,我们此时就直接关联用户的基础dto,而不是关联dto

这样基本就解除了循环引用问题

ps:实际上问题循环引用最头疼是关联一对一,这样的错误会连项目都跑不起来,一对多多对多目前不存在这种跑步起来的情况,另外按照上面说法改进,能避免不少问题

运行

通过上面我们额外增加了和 prisma 相关的 scripts

"prisma:merge": "prisma-import -f -s ./prisma/schemas/**/*.prisma -o ./prisma/schema.prisma",
"generate": "prisma generate",
"combin": "prisma-import -f -s ./prisma/schemas/**/*.prisma -o ./prisma/schema.prisma && prisma generate",
"migrate": "npx prisma migrate dev --name init",
"db:push": "npx prisma db push",
"dev": "prisma-import -f -s ./prisma/schemas/**/*.prisma -o ./prisma/schema.prisma && prisma generate && nest start --watch", //在加上这个dev可以吧,或者是 yarn combin && yarn start:dev 也行

一般开发运行时,我们会需要运行 prisma:merge + generate,直接被我合并到了 combin

当改动数据库时,除了上面两个,还需要 db:push,然后可以运行 yarn start(yarn start:dev) 尝试运行结果

如果有些改动会刺激到了数据库丢失数据,那么可以使用迁移功能,迁移功能会生成一些迁移文件,帮助我们迁移,详细自己看文档,就不多介绍了,篇幅有点大了

ps:并不是所有数据库模型改动会让数据库信息丢失,即使是他给你提示了可能会丢失,实际上一些操作是不会丢失的,例如:以前没设置 unique 操作,但是现在设置了,可能由于信息重复会丢失数据,但是以前由于在服务器查询做了唯一校验(小概率重复),实际数据并没有出现重复时,直接就可以设置成功,不会丢失数据等等,怕丢失信息,一般都是通过数据迁移备份的方式,老司机也不敢说百分百不会丢失(因为怕误操作,是人就会犯错误)

最后

nest 接入 prisma,已经完成了,如果是想用的舒服,还是建议使用 typeorm,这个 prisma 确实无论是成熟程度还是契合度,比起来都要差点,不过还是看个人爱好和情况吧,毕竟 prisma 效率确实高一点,尤其是一个人开发的时候