Skip to content
Linbudu's Blog

【Prisma,下一代ORM,不仅仅是ORM(上篇)

4 min read

前言

本篇文章将会介绍一个 NodeJS 社区中的 ORM:Prisma。我接触它的时间不算长,但已经对它的未来发展充满信心。这篇文章其实三个月以前就写了一部分,所以文中会出现“如果你觉得它不错,不如考虑基于 Prisma 来完成你的毕设”这样的话。

在刚开始写的时候,bven爷的毕设一行都还没动,而到了我今天发的时候,他已经是优秀毕业生了...

同时,原本准备一篇搞定所有内容,但是觉得这种教程类的文章如果写的这么长,很难让人有读完的兴致。所以就拆成了两部分:

  • 第一部分主要是铺垫,介绍目前 NodeJS 社区比较主流的 ORM 与 Query Builder,以及 Prisma 的简单使用,这一部分主要是为接触 ORM 较少的同学做一个基础知识的铺垫。
  • 第二部分包括 Prisma 的花式进阶使用,包括多表级联、多数据库协作以及与 GraphQL 的实战,最后会展开来聊一聊 Prisma 的未来。

文章的大致顺序如下:

  • NodeJS 社区中的老牌、传统 ORM
  • 传统 ORM 的 Data Mapper 与 Active Record 模式
  • Query Builder
  • Prisma 的基础环境配置
  • Hello Prisma
  • 从单表 CRUD 开始
  • 多表、多数据库实战
  • Prisma 与 GraphQL:全链路类型安全
  • Prisma 与一体化框架

NodeJS 社区中的 ORM

经常写 Node 应用的同学通常免不了要和 ORM 打交道,毕竟写原生 SQL 对于大部分前端同学来说真的是一种折磨。ORM 的便利性使得很多情况下我们能直观而方便的和数据库打交道(虽然的确有些情况下 ORM 搞不定),用我们熟悉的 JavaScript 来花式操作数据库。 NodeJS 社区中主流的 ORM 主要有这么几个,它们都有各自的一些特色:

  • Sequelize,比较老牌的一款 ORM,缺点是 TS 支持不太好,但是社区有Sequelize-TypeScript

    Sequelize 定义表结构的方式是这样的:

    1const { Sequelize, Model, DataTypes } = require("sequelize");
    2const sequelize = new Sequelize("sqlite::memory:");
    3
    4class User extends Model {}
    5
    6User.init(
    7 {
    8 username: DataTypes.STRING,
    9 birthday: DataTypes.DATE,
    10 },
    11 { sequelize, modelName: "user" }
    12);
    13
    14(async () => {
    15 await sequelize.sync();
    16 const jane = await User.create({
    17 username: "janedoe",
    18 birthday: new Date(1980, 6, 20),
    19 });
    20 console.log(jane.toJSON());
    21})();

    (我是觉得不那么符合直觉,所以我只在入门时期简单使用过)

  • TypeORM,NodeJS 社区 star 最多的一个 ORM。也确实很好用,在我周围的同学里备受好评,同时也是我自己用的最多的一个 ORM。亮点在基于装饰器语法声明表结构、事务、级联等,以及很棒的 TS 支持。

    TypeORM 声明表结构是这样的:

    1import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
    2
    3@Entity()
    4export class User {
    5 @PrimaryGeneratedColumn()
    6 id: number;
    7
    8 @Column()
    9 firstName: string;
    10
    11 @Column()
    12 lastName: string;
    13
    14 @Column()
    15 age: number;
    16}

    比起 Sequelize 来要直观的多,而且由于通过类属性的方式来定义数据库字段,可以很好的兼容 Mixin 以及其他基于类属性的工具库,如TypeGraphQL

  • MikroORM,比较新的一个 ORM,同样大量基于装饰器语法,亮点在于自动处理所有事务以及表实体会在全局保持单例模式,暂时还没深入使用过过。

    MikroORM 定义表结构方式是这样的:

    1@Entity()
    2export class Book extends BaseEntity {
    3 @Property()
    4 title!: string;
    5
    6 @ManyToOne()
    7 author!: Author;
    8
    9 @ManyToOne()
    10 publisher?: IdentifiedReference<Publisher>;
    11
    12 @ManyToMany({ fixedOrder: true })
    13 tags = new Collection<BookTag>(this);
    14}
  • MongooseTypegoose,MongoDB 专用的 ORM,这里简单放一下 TypeGoose 的使用示例:

    1import { prop, getModelForClass } from "@typegoose/typegoose";
    2import * as mongoose from "mongoose";
    3
    4class User {
    5 @prop()
    6 public name?: string;
    7
    8 @prop({ type: () => [String] })
    9 public jobs?: string[];
    10}
    11
    12const UserModel = getModelForClass(User);
    13
    14(async () => {
    15 await mongoose.connect("mongodb://localhost:27017/", {
    16 useNewUrlParser: true,
    17 useUnifiedTopology: true,
    18 dbName: "test",
    19 });
    20
    21 const { _id: id } = await UserModel.create({
    22 name: "JohnDoe",
    23 jobs: ["Cleaner"],
    24 } as User);
    25
    26 const user = await UserModel.findById(id).exec();
    27
    28 console.log(user);
    29})();
  • Bookshelf,一个相对简单一些但也五脏俱全的 ORM,基于 Knex(Strapi 底层的 Query Builder,后面会简单介绍)。它的使用方式大概是这样的:

    1const knex = require("knex")({
    2 client: "mysql",
    3 connection: process.env.MYSQL_DATABASE_CONNECTION,
    4});
    5// bookshelf 基于 knex,所以需要实例化knex然后传入
    6const bookshelf = require("bookshelf")(knex);
    7
    8const User = bookshelf.model("User", {
    9 tableName: "users",
    10 posts() {
    11 return this.hasMany(Posts);
    12 },
    13});
    14
    15const Post = bookshelf.model("Post", {
    16 tableName: "posts",
    17 tags() {
    18 return this.belongsToMany(Tag);
    19 },
    20});
    21
    22const Tag = bookshelf.model("Tag", {
    23 tableName: "tags",
    24});
    25
    26new User({ id: 1 })
    27 .fetch({ withRelated: ["posts.tags"] })
    28 .then((user) => {
    29 console.log(user.related("posts").toJSON());
    30 })
    31 .catch((error) => {
    32 console.error(error);
    33 });

    另外,一个比较独特的地方是 bookshelf 支持了插件机制,其他 ORM 通常通过 hook 或者 subscriber 的方式实现类似的功能,如密码存入时进行一次加密、TPS 计算、等。

ORM 的 Data Mapper 与 Actice Record 模式

如果你去看了上面列举的 ORM 文档,你会发现 MikroORM 的简介中包含这么一句话:TypeScript ORM for Node.js based on Data Mapper,而 TypeORM 的简介中则是TypeORM supports both Active Record and Data Mapper patterns

先来一个问题,使用 ORM 的过程中,你是否了解过 Data MapperActive Record 这两种模式的区别?

先来看看 TypeORM 中分别是如何使用这两种模式的:

Active Record:

1import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
2
3@Entity()
4export class User extends BaseEntity {
5 @PrimaryGeneratedColumn()
6 id: number;
7
8 @Column()
9 name: string;
10
11 @Column()
12 isActive: boolean;
13}
14
15const user = new User();
16user.name = "不渡";
17user.isActive = true;
18
19await user.save();
20
21const newUsers = await User.find({ isActive: true });

TypeORM 中,Active Record 模式下需要让实体类继承BaseEntity类,这样实体类上就具有了各种方法,如save remove find方法等。Active Record 模式最早由 Martin Fowler企业级应用架构模式 一书中命名,这一模式使得对象上拥有了相关的 CRUD 方法。在RoR中就使用了这一模式来作为 MVC 中的 M,即数据驱动层。如果你对 RoR 中的 Active Record 有兴趣,可以阅读 全面理解 Active Record(我不会 Ruby,因此就不做介绍了)。

Data Mapper:

1import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
2
3@Entity()
4export class User {
5 @PrimaryGeneratedColumn()
6 id: number;
7
8 @Column()
9 name: string;
10
11 @Column()
12 isActive: boolean;
13}
14
15const userRepository = connection.getRepository(User);
16
17const user = new User();
18user.name = "不渡";
19user.isActive = true;
20
21await userRepository.save(user);
22
23await userRepository.remove(user);
24
25const newUsers = await userRepository.find({ isActive: true });

可以看到在 Data Mapper 模式中,实体类不再能够自己进行数据库操作,而是需要先获取到一个对应到表的“仓库”,然后再调用这个“仓库”上的方法。

这一模式同样由Martin Fowler最初命名,Data Mapper 更像是一层拦在操作者与实际数据之间的访问层,就如上面例子中先获取具有访问权限(即相应方法)的对象,再进行数据的操作。

对这两个模式进行比较,很容易发现 Active Record 模式要简单的多,而 Data Mapper 模式则更加严谨。那么何时使用这两种模式就很清楚了,如果你在开发比较简单的应用,直接使用 Active Record 模式就好了,因为这确实会减少很多代码。但是如果你在开发规模较大的应用,使用 Data Mapper 模式则能够帮助你更好的维护代码(实体类不再具有访问数据库权限了,只能通过统一的接口(getRepository getManager等)),一个例子是在 Nest、Midway 这两个 IoC 风格的 Node 框架中,均使用 Data Mapper 模式注入 Repository 实例,然后再进行操作。

最后,NodeJS 中使用 Data Mapper 的 ORM 主要包括 Bookshelf、MikroORM、objection.js以及本文主角 Prisma 等。

Query Builder

实际上除了 ORM 与原生 SQL 以外,还有一种常用的数据库交互方式:Query Builder(以下简称 QB)。

QB 和 ORM 其实我个人觉得既有相同之处又有不同之处,但是挺容易搞混,比如 MQuery (MongoDB 的一个 Query Builder)的方法是这样的:

1mquery().find(match, function (err, docs) {
2 assert(Array.isArray(docs));
3});
4
5mquery().findOne(match, function (err, doc) {
6 if (doc) {
7 // the document may not be found
8 console.log(doc);
9 }
10});
11
12mquery().update(match, updateDocument, options, function (err, result) {});

是不是看起来和 ORM 很像?但我们再看看其他的场景:

1mquery({ name: /^match/ })
2 .collection(coll)
3 .setOptions({ multi: true })
4 .update({ $addToSet: { arr: 4 } }, callback);

在 ORM 中,通常不会存在这样的多个方法链式调用,而是通过单个方法+多个参数的方式来操作,这也是 Query Builder 和 ORM 的一个重要差异。再来看看 TypeORM 的 Query Builder 模式:

1import { getConnection } from "typeorm";
2
3const user = await getConnection()
4 .createQueryBuilder()
5 .select("user")
6 .from(User, "user")
7 .where("user.id = :id", { id: 1 })
8 .getOne();

以上的操作其实就相当于userRepo.find({ id: 1 }),你可能会觉得 QB 的写法过于繁琐,但实际上这种模式要灵活的多,和 SQL 语句的距离也要近的多(你可以理解为每一个链式方法调用都会对最终生成的 SQL 语句进行一次操作)。

同时在部分情境(如多级级联下)中,Query Builder 反而是代码更简洁的那一方,如:

1const selectQueryBuilder = this.executorRepository
2 .createQueryBuilder("executor")
3 .leftJoinAndSelect("executor.tasks", "tasks")
4 .leftJoinAndSelect("executor.relatedRecord", "records")
5 .leftJoinAndSelect("records.recordTask", "recordTask")
6 .leftJoinAndSelect("records.recordAccount", "recordAccount")
7 .leftJoinAndSelect("records.recordSubstance", "recordSubstance")
8 .leftJoinAndSelect("tasks.taskSubstance", "substance");

以上代码构建了一个包含多张表的级联关系的 Query Builder。

级联关系如下:

  • Executor
    • tasks -> Task
    • relatedRecord -> Record
  • Task
    • substances -> Substance
  • Record
    • recordTask -> Task
    • recordAccount -> Account
    • recordSubstance -> Substance

再看一个比较主流的 Query Builder knex,我是在尝鲜strapi的过程中发现的,strapi 底层依赖于 knex 去进行数据库交互以及连接池相关的功能,knex 的使用大概是这样的:

1const knex = require("knex")({
2 client: "sqlite3",
3 connection: {
4 filename: "./data.db",
5 },
6});
7
8try {
9 await knex.schema
10 .createTable("users", (table) => {
11 table.increments("id");
12 table.string("user_name");
13 })
14 .createTable("accounts", (table) => {
15 table.increments("id");
16 table.string("account_name");
17 table.integer("user_id").unsigned().references("users.id");
18 });
19
20 const insertedRows = await knex("users").insert({ user_name: "Tim" });
21
22 await knex("accounts").insert({
23 account_name: "knex",
24 user_id: insertedRows[0],
25 });
26
27 const selectedRows = await knex("users")
28 .join("accounts", "users.id", "accounts.user_id")
29 .select("users.user_name as user", "accounts.account_name as account");
30
31 const enrichedRows = selectedRows.map((row) => ({ ...row, active: true }));
32} catch (e) {
33 console.error(e);
34}

可以看到 knex 的链式操作更进了一步,甚至可以链式创建多张数据库表。

Prisma

接下来就到了我们本篇文章的主角:Prisma 。Prisma 对自己的定义仍然是 NodeJS 的 ORM,但个人感觉它比普通意义上的 ORM 要强大得多。这里放一张官方的图,来大致了解下 Prisma 和 ORM、SQL、Query Builder 的能力比较:

comparison

你也可以阅读方方老师翻译的这篇Why Prisma?来了解更多。

独特的 Schema 定义方式、比 TypeORM 更加严谨全面的 TS 类型定义(尤其是在级联关系中)、更容易上手和更贴近原生 SQL 的各种操作符等,很容易让初次接触的人欲罢不能(别说了,就是我)。

简单的介绍下这些特点:

  • Schema 定义,我们前面看到的 ORM 都是使用 JS/TS 文件来定义数据库表结构的,而 Prisma 不同,它使用.prisma后缀的文件来书写独特的 Prisma Schema,然后基于 schema 生成表结构,VS Code 有 prisma 官方提供的高亮、语法检查插件,所以不用担心使用负担。

    同时,这也就意味着围绕 Prisma Schema 会产生一批 generator 功能的生态,如 typegraphql-prisma 就能够基于 Prisma Schema 生成 TypeGraphQL 的 Class 定义,甚至还有 CRUD 的基本 Resolver,类似的还有palJS提供的基于 Prisma Schema 生成 Nexus 的类型定义与 CRUD 方法(所以说 GraphQL 和 Prisma 这种都是 SDL-First 的工具真的是天作之合)。

    TypeGraphQL、Resolver 属于 GraphQL 相关的工具/概念,如果未曾了解过也不要紧。

    一个简单的schema.prisma可能是这样的:

    1datasource db {
    2 provider = "sqlite"
    3 url = env("SINGLE_MODEL_DATABASE_URL")
    4}
    5
    6generator client {
    7 provider = "prisma-client-js"
    8 output = "./client"
    9}
    10
    11model Todo {
    12 id Int @id @default(autoincrement())
    13 title String
    14 content String?
    15 finished Boolean @default(false)
    16 createdAt DateTime @default(now())
    17 updatedAt DateTime @updatedAt
    18}

    是不是感觉即使你没用过,但还是挺好看懂。

  • TS 类型定义,可以说 Prisma 的类型定义是全覆盖的,查询参数、操作符参数、级联参数、返回结果等等,比 TypeORM 的都更加完善。

  • 更全面的操作符,如对字符串的查询,Prisma 中甚至提供了 contains、startsWith、endsWith 这种细粒度的操作符供过滤使用(而 TypeORM 中只能使用ILike这种方法来全量匹配)。(这些操作符的具体作用我们会在后面讲到)

在这一部分的最后,我们来简单的介绍下 Prisma 的使用流程,在正文中,我们会一步步详细介绍 Prisma 的使用,包括单表、多表级联以及 Prisma 与 GraphQL 的奇妙化学反应。

环境配置在下一节,这里我们只是先感受一下使用方式

  • 首先,创建一个名为prisma的文件夹,在内部创建一个schema.prisma文件

    如果你使用的是 VS Code,可以安装 Prisma 扩展来获得.prisma的语法高亮

  • 在 schema 中定义你的数据库类型、路径以及你的数据库表结构,示例如下:

    1model Todo {
    2 id Int @id @default(autoincrement())
    3 title String
    4}
  • 运行prisma generate命令,prisma 将为你生成Prisma Client,内部结构是这样的:

    image-20210324100621450

  • 在你的文件中导入Prisma Client即可使用:

    1import { PrismaClient } from "./prisma/client";
    2
    3const prisma = new PrismaClient();
    4
    5async function createTodo(title: string, content?: string) {
    6 const res = await prisma.todo.create({
    7 data: {
    8 title,
    9 content,
    10 },
    11 });
    12 return res;
    13}

    每张表都会被存放在prisma.__YOUR_MODEL__的命名空间下。

如果看完简短的介绍你已经感觉这玩意有点好玩了,那么在跟着本文完成实践后,你可能也会默默把手上的项目迁移到 Prisma(毕设也可以安排上)~

上手 Prisma

你可以在 Prisma-Article-Example 找到完整的示例,以下的例子我们会从一个空文件夹开始。

项目初始化

  • 创建一个空文件夹,执行npm init -y

    yarn、pnpm 同理

  • 全局安装@prisma/clinpm install prisma -g

    @prisma/cli 包已被更名为 prisma

    全局安装@prisma/cli是为了后面执行相关命令时方便些~

  • 安装必要的依赖:

    1npm install @prisma/client sqlite3 prisma -S
    2npm install typescript @types/node nodemon ts-node -D

    安装prisma到文件夹时会根据你的操作系统下载对应的 Query Engine:

  • 执行prisma version,确定安装成功。

    image-20210324102342154

  • 执行prisma init,初始化一个 Prisma 项目(这个命令的侵入性非常低,只会生成prisma文件夹和.env文件,如果.env文件已经存在,则会将需要的环境变量追加到已存在的文件)。

    image-20210324102523696

  • 查看.env文件

    1# Environment variables declared in this file are automatically made available to Prisma.
    2# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
    3
    4# Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
    5# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
    6
    7DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

    你会发现这里的数据库默认使用的是 postgresql,在本文中为了降低学习成本,我们全部使用 SQLite 作为数据库,因此需要将变量值修改为file:../demo.sqlite

    如果你此前没有接触过 SQLite,可以理解为这是一个能被当作数据库读写的文件(.sqlite后缀),因此使用起来非常容易,也正是因为它是文件,所以需要将DATABASE_URL这一变量改为file://协议。

    同样的,在 Prisma Schema 中我们也需要修改数据库类型为sqlite

    1// This is your Prisma schema file,
    2// learn more about it in the docs: https://pris.ly/d/prisma-schema
    3
    4datasource db {
    5 provider = "sqlite"
    6 url = env("DATABASE_URL")
    7}
    8
    9generator client {
    10 provider = "prisma-client-js"
    11}

创建数据库

在上面的 Prisma Schema 中,我们只定义了 datasource 和 generator,它们分别负责定义使用的数据库配置和客户端生成的配置,举例来说,默认情况下 prisma 生成的 client 会被放置在 node_modules 下,导入时的路径也是import { PrismaClient } from "@prisma/client",但你可以通过client.output命令更改生成的 client 位置。

1generator client {
2 provider = "prisma-client-js"
3 output = "./client"
4}

这一命令会使得 client 被生成到prisma文件夹下,如:

image-20210612174918265

将 client 生成到对应的 prisma 文件夹下这一方式使得在 monorepo(或者只是多个文件夹的情况)下,每个项目可以方便的使用不同配置的 schema 生成的 client。

我们在 Prisma Schema 中新增数据库表结构的定义:

1datasource db {
2 provider = "sqlite"
3 url = env("SINGLE_MODEL_DATABASE_URL")
4}
5
6generator client {
7 provider = "prisma-client-js"
8 output = "./client"
9}
10
11model Todo {
12 id Int @id @default(autoincrement())
13 title String
14 content String?
15 finished Boolean @default(false)
16 createdAt DateTime @default(now())
17 updatedAt DateTime @updatedAt
18}

简单解释下相关语法:

  • Int、String 等这一类标量会被自动基于数据库类型映射到对应的数据类型。标量类型后的?意味着这一字段是可选的。
  • @id 意为标识此字段为主键,@default()意为默认值,autoincrementnow为 prisma 内置的函数,分别代表自增主键与字段写入时的时间戳,类似的内置函数还有 uuid、cuid 等。

客户端生成与使用

现在你可以生成客户端了,执行prisma generate

image-20210324105558187

还没完,我们的数据库文件(即 sqlite 文件)还没创建出来,执行prisma db push

image-20210324110551303

这个命令也会执行一次prisma generate,你可以使用--skip-generate跳过这里的 client 生成。

现在根目录下就出现了demo.sqlite文件。

在根目录下创建 index.ts:

1// index.ts
2import { PrismaClient } from "./prisma/client";
3
4const prisma = new PrismaClient();
5
6async function main() {
7 console.log("Prisma!");
8}
9
10main();

从使用方式你也可以看出来PrismaClient实际上是一个类,所以你可以继承这个类来进行很多扩展操作,在后面我们会提到。

在开始使用前,为了后续学习的简洁,我们使用nodemon + ts-node,来帮助我们在 index.ts 发生变化时自动重新执行。

1{
2 "name": "Prisma2-Explore",
3 "restartable": "r",
4 "delay": "500",
5 "ignore": [".git", "node_modules/**", "/prisma/*"],
6 "verbose": true,
7 "execMap": {
8 "": "node",
9 "js": "node --harmony",
10 "ts": "ts-node "
11 },
12 "watch": ["./**/*.ts"]
13}

并将启动脚本添加到 package.json:

1{
2 "scripts": {
3 "start": "nodemon index.ts"
4 }
5}

执行npm start

image-20210324110144559

Prisma 单表初体验

环境配置

接下来就到了正式使用环节,上面的代码只是一个简单的开发工作流示范,本文接下来的部分不会使用到(但是你可以基于这个工作流自己进一步的探索 Prisma)。

在接下来,你所需要的相关环境我已经准备完毕,见Prisma-Article-Example,clone 仓库到本地,运行配置完毕的 npm scripts 即可。在这里简单的介绍下项目中的 npm scripts,如果在阅读完毕本部分内容后觉得意犹未尽,可以使用这些 scripts 直接运行其他部分如多表、GraphQL 相关的示例。简单介绍部分 scripts:

  • yarn flow:从零开始完整的执行 生成客户端 - 构建项目 - 执行构建产物 的流程。
  • yarn dev:**:在开发模式下运行项目,文件变化后重启进程。
  • yarn generate:**:为项目生成 Prisma Client。
    • 使用 yarn gen:client来为所有项目生成 Prisma Client。
  • yarn setup:**:为构建完毕的项目生成 SQLite 文件。
  • yarn invoke:**:执行构建后的 JS 文件。
    • 使用yarn setup执行所有构建后的 JS 文件。

本部分(Prisma 单表示例)的代码见 single-model,相关的命令包括:

1$ yarn dev:single
2$ yarn generate:single
3$ yarn setup:single
4$ yarn invoke:single

在开始下文的 CRUD 代码讲解时,最好首先运行起来项目。首先执行yarn generate:single,生成 Prisma Client,然后再yarn dev:single,进入开发模式,如下:

image-20210613205500970

我直接一顿 CRUD

根据前面已经提到的使用方式,首先引入 Prisma Client 并实例化:

1import { PrismaClient } from "./prisma/client";
2
3const prisma = new PrismaClient();

Prisma 将你的表类(Table Class)挂载在prisma.MODEL下,MODEL值直接来自于schema.prisma中的 model 名称,如本例是Todo,那么就可以在prisma.todo下获取到相关的操作方法:

image-20210613210333704

因此,简单的 CRUD 完全可以直接照着 API 来,

创建:

1async function createTodo(title: string, content?: string) {
2 const res = await prisma.todo.create({
3 data: {
4 title,
5 content: content ?? null,
6 },
7 });
8 return res;
9}

create 方法接受两个参数:

  • data,即你要用来创建新数据的属性,类型定义由你的 schema 决定,如这里 content 在 schema 中是可选的字符串(String?),其类型就为string|null,所以需要使用??语法来照顾参数未传入的情况。
  • select,决定 create 方法返回的对象中的字段,如果你指定select.id为 false,那么 create 方法的返回值对象中就不会包含 id 这一属性。这一参数在大部分 prisma 方法中都包含。

读取:

1async function getTodoById(id: number) {
2 const res = await prisma.todo.findUnique({
3 where: { id },
4 });
5 return res;
6}

findUnique 方法类似于 TypeORM 中的 findOne 方法,都是基于主键查询,在这里将查询条件传入给 where 参数。

读取所有:

1async function getTodos(status?: boolean) {
2 const res = await prisma.todo.findMany({
3 orderBy: [{ id: "desc" }],
4 where: status
5 ? {
6 finished: status,
7 }
8 : {},
9 select: {
10 id: true,
11 title: true,
12 content: true,
13 createdAt: true,
14 },
15 });
16 return res;
17}

在这里我们额外传入了 orderBy 方法来对返回的查询结果进行排序,既然有了排序,当然也少不了分页。你还可以传入cursorskiptake等参数来完成分页操作。

cursor-based 与 offset-based 实际上是两种不同的分页方式。

类似的,更新操作:

1async function updateTodo(
2 id: number,
3 title?: string,
4 content?: string,
5 finished?: boolean
6) {
7 const origin = await prisma.todo.findUnique({
8 where: { id },
9 });
10
11 if (!origin) {
12 throw new Error("Item Inexist!");
13 }
14
15 const res = await prisma.todo.update({
16 where: {
17 id,
18 },
19 data: {
20 title: title ?? origin.title,
21 content: content ?? origin.content,
22 finished: finished ?? origin.finished,
23 },
24 });
25 return res;
26}

这里执行的是在未查询到主键对应的数据实体时抛出错误,你也可以使用 upsert 方法来在数据实体不存在时执行创建。

批量更新:

1async function convertStatus(status: boolean) {
2 const res = await prisma.todo.updateMany({
3 where: {
4 finished: !status,
5 },
6 data: {
7 finished: {
8 set: status,
9 },
10 },
11 });
12
13 return res;
14}

注意,这里我们使用 set 属性,来直接设置 finished 的值。这一方式和直接设置其为 false 是效果一致的,如果这里是个 number 类型,那么除了 set 以外,还可以使用 increment、decrement、multiply 以及 divide 方法。

最后是删除操作:

1async function deleteTodo(id: number) {
2 const res = await prisma.todo.delete({
3 where: { id },
4 });
5 return res;
6}
7
8async function clear() {
9 const res = await prisma.todo.deleteMany();
10 return res;
11}

你可以自由的在以上这些例子以外,借助良好的 TS 类型提示花式探索 Prisma 的 API,也可以提前看看其它部分的例子来早一步感受 Prisma 的强大能力。

尾声 & 下篇预告

以上使用到的 Prisma 方法(如 create)与操作符(如 set)只是一小部分,目的只是为了让你大致感受下 Prisma 与其他传统 ORM 相比新奇的使用方式。在下篇中,我们将会介绍:

  • Prisma 多张数据表的级联关系处理
  • 多个 Prisma Client 协作
  • Prisma 与其他 ORM 的协作
    • 和上一项一样都属于
  • Prisma + GraphQL 全流程实战
  • Prisma 的展望:工作原理、一体化框架