DTeam 技术日志

Doer、Delivery、Dream

Node ORM 框架 Prisma 快速上手

冯宇 Posted at — Jan 6, 2022 阅读

简介

Prisma 是一个 开源 的下一代 ORM。它包含了以下部分:

Prisma 客户端可以被用在 任何 Node.js 或 TypeScript 后端应用中(包括 Serverless 应用和微服务)。可以是一个 REST API,一个 GraphQL API,一个 gRPC API,或任何其他需要数据库的东西。

本文将快速介绍 Prisma 基本使用方法。

官方 Quickstart 示例

直接访问 官方示例

创建工程模板

创建 typescript 工程,使用 gts:

mkdir -p hello-prisma
cd hello-prisma
npx gts init -y
npm install -D @vercel/ncc

安装 prisma 依赖:

npm install -D prisma

稍微调整下tsconfig.json的内容如下(gts 使用的module配置为commonjs,主要这个需要调整下):

{
  "extends": "./node_modules/gts/tsconfig-google.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "module": "ES2020",
    "target": "ES2020",
    "lib": ["ES2020"],
    "esModuleInterop": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts", "test/**/*.ts"]
}

初始化 prisma 模板:

$ npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

prisma cli 已经帮我们生成了一个 schema 模板 prisma/schema.prisma:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

以及一个dotenv配置文件.env:

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

基本工作流程

调整数据库连接方式,在这个基础上追加表结构定义就可以了。比如我们定义一个 user 表:

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String?
}

model 命名需要满足大写驼峰式,并且不能使用保留字,参考官方文档 prisma-schema-reference#naming-conventions

运行 npx prisma migrate dev --name init 即可创建数据库并生成 migration 脚本:

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "email" TEXT NOT NULL,
    "name" TEXT,

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

本地进行快速原型设计还可以用 db push 直接修改数据库,而不创建 migrate 脚本,这在本地快速迭代,不关心中间的变更场景下很有用。

但在产品环境期望不丢失数据而安全迁移数据库的场景下,应该用 migrate deploy 而非 db push。有关 db push 的描述以及与 migrate 的对比参见官方文档: db-push

修改 schema ,增加一个字段:

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String?
  // 增加一个 foo 字段,定义为 VARCHAR 类型
  foo       String?  @db.VarChar(10)
}

运行 npx prisma migrate dev --name add_foo_column,生成的 sql 如下:

-- AlterTable
ALTER TABLE "User" ADD COLUMN     "foo" VARCHAR(10);

以后修改 prisma/schema.prisma 之后再次运行 npx prisma migrate dev 即可将改动同步到数据库中。如果追加 --create-only 参数,则只生成 migration.sql 文件,不会同步到数据库中。

开发环境生成 migrate 脚本(主要是 migrate devmigrate reset 命令需要用 shadow database)需要有数据库的 createdb 权限,否则会报错。如果你的环境无法提供 createdb 权限,你需要单独创建一个 shadow database,在 datasource db {} 配置块中额外添加 shadowDatabaseUrl = env('SHADOW_DATABASE_URL') 指定,参考官方文档: shadow-database

产品环境直接用 migrate deploymigrate resolve 运行 migrate 脚本,无需创建 shadow database。

Schema 定义

注意 Prisma 维护了一组默认的 schema 到数据库类型的映射,如果不满足你的需求,可以追加 @db.<database_type> 参数变更,如 String 类型默认映射为数据库的 TEXT 类型,可能你希望映射成 VARCHAR 类型:

model User {
  userName String @db.VarChar(10)
}

prisma schema 所有支持的字段类型以及可以映射的数据库类型(@db.<database_type>)可以参考官方文档: model-field-scalar-types

其他常用的 schema 定义

注释

prisma 支持两种注释 /////,前者只是代码普通注释,后者会出现在节点语法树中(AST),以 Data Model Meta Format (DMMF)形式呈现:

/// This comment will get attached to the `User` node in the AST
model User {
  /// This comment will get attached to the `id` node in the AST
  id     Int   @default(autoincrement())
  // This comment is just for you
  weight Float /// This comment gets attached to the `weight` node
}

// This comment is just for you. It will not
// show up in the AST.

/// This comment will get attached to the
/// Customer node.
model Customer {}

当前 prisma 还不支持数据库级别的表和字段注释(DDL COMMENT),参见官方issue

目前只能通过 --create-only 参数生成 sql 之后手改

表/字段重命名

为了规避数据库的大小写问题,一般数据库最佳实践都是表和字段统一使用小写下划线形式命名。对于 prisma 需要使用 @@map@map 函数定义:

model User {
  id     Int @id
  // @map 用于定义列名
  cardId Int @map("card_id")

  // @@map 用于定义表名
  @@map("user")
}

对应生成的 sql 如下:

-- CreateTable
CREATE TABLE "user" (
    "id" INTEGER NOT NULL,
    "card_id" INTEGER NOT NULL,

    CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);

如果要对索引、约束等命名的话参考官方文档: names-in-underlying-databas#using-custom-constraint–index-names

字段默认值(@default)

@default 可以定义字段的默认值,参数可以是静态的固定值,如5, false等等,也可以是 prisma 提供的几种函数,如autoincrement(), uuid(), now() 等等,参见文档: prisma-schema-reference#default

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
}

枚举类型

可以定义字段为枚举类型:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  role  Role    @default(USER)
}

enum Role {
  USER
  ADMIN
}

对应 sql:

-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');

-- CreateTable
CREATE TABLE "User" (
    "id" SERIAL NOT NULL,
    "email" TEXT NOT NULL,
    "name" TEXT,
    "role" "Role" NOT NULL DEFAULT E'USER',

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

自动存储更新时间(@updatedAt)

这个属性可以自动设置更新时间:

model Post {
  id        String   @id
  createdAt DateTime @default(now()) @db.Timestamptz
  updatedAt DateTime @updatedAt @db.Timestamptz
}

索引(@@index)

直接使用 @@index 函数可以创建索引:

model Post {
  id      Int     @id @default(autoincrement())
  title   String
  content String?

  @@index([title, content])
}

如果要自定义索引类型,可以用 npx prisma migrate dev --create-only 生成 migration.sql 文件,然后在 migration.sql 中修改。

如果期望在 schema 文件中定义,则需要启用 extendedIndexes preview feature:

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["extendedIndexes"]
}

就可以在 @@index 中使用 type 参数指定索引类型了:

model Example {
  id    Int @id
  value Int

  @@index([value], type: Hash)
}

有关 @@index 部分的文档参考: prisma-schema-reference#index

extendedIndexes 部分的文档参考: indexes

字段约束

主键(@id / @@id)

单主键

model Table {
  id Int @id @default(autoincrement())
}

生成 sql:

-- CreateTable
CREATE TABLE "Table" (
    "id" SERIAL NOT NULL,

    CONSTRAINT "Table_pkey" PRIMARY KEY ("id")
);

联合主键

model Table {
  firstName String @db.VarChar(10)
  lastName  String @db.VarChar(10)

  @@id([firstName, lastName])
}

生成 sql:

-- CreateTable
CREATE TABLE "Table" (
    "firstName" VARCHAR(10) NOT NULL,
    "lastName" VARCHAR(10) NOT NULL,

    CONSTRAINT "Table_pkey" PRIMARY KEY ("firstName","lastName")
);

唯一性(@unique / @@unique)

单列唯一性

model User {
  id    Int     @id @default(autoincrement())
  email String? @unique
  name  String
}

多列唯一性

model User {
  id        Int  @id
  firstname Int
  lastname  Int
  card      Int?

  @@unique([firstname, lastname, card])
}

非空约束

默认字段类型都是非空的,可空给字段类型加上 ? 就行了:

model User {
  id        Int  @id
  firstname Int
  lastname  Int
  card      Int?
}

外键约束(@relation)

稍微麻烦些,需要同时在两个 model 定义互相的对应关系,如下所示:

model User {
  id        Int    @id
  firstname String @db.VarChar(10)
  lastname  String @db.VarChar(10)
  Post      Post[]
}

model Post {
  id      Int    @id
  author  User   @relation(fields: [userId], references: [id])
  content String @db.VarChar(200)
  userId  Int
}

生成 sql:

-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL,
    "firstname" VARCHAR(10) NOT NULL,
    "lastname" VARCHAR(10) NOT NULL,

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Post" (
    "id" INTEGER NOT NULL,
    "content" VARCHAR(200) NOT NULL,
    "userId" INTEGER NOT NULL,

    CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

这里有个比较方便的方法可以快速创建出这种映射关系,就是直接利用 prisma format 的功能即可。

比如先像 Hibernate 那样创建表关联:

model User {
  id        Int    @id
  firstname String @db.VarChar(10)
  lastname  String @db.VarChar(10)
}

model Post {
  id      Int    @id
  author  User
  content String @db.VarChar(200)
}

然后在命令行下执行 npx prisma format,或者在 vscode 安装插件Prisma后直接在 vscode 格式化即可自动生成上述 model 映射关系。

更多映射关系(一对一,一对多,多对多)的定义可以参考官方文档: relations

使用 Prisma Client

定义好 schema,完成 migrate 之后,需要通过 prisma-client 调用 schema 完成数据库的操作。

npm install @prisma/client

第一次安装 @prisma/client 之后,会自动调用 prisma generate 生成定制化的 client,以后每次修改了 schema 之后,需要手工调用 npx prisma generate 生成新的 client model。

在项目中引入 PrismaClient 组件即可:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

想要打印 sql 可以这么配置:

const prisma = new PrismaClient({
  log: ["query", "info", "warn", "error"],
});

PrismaClient 默认使用连接池,关于连接池的配置可以参考官方文档: connection-pool

基本 CURD 操作

有关详细的基本 CURD 操作直接参考官方文档完整的示例: crud

都比较简单,一目了然。基本的基于 model 的 CURD 函数主要有下面几个:

此外,还可以通过 select 属性控制返回的字段:

// Returns an object or null
const getUser: object | null = await prisma.user.findUnique({
  where: {
    id: 22,
  },
  select: {
    email: true,
    name: true,
  },
});

更多详情参见: select-fields

当前 prisma 还不支持 exclude 排除特定的字段(如 password),官方正在对这个设计进行探讨中,参见 issue: prisma/prisma#5042

如果用到类似于 join 之类的关联查询,可以参考文档: relation-queries

对于 JSONB 相关的查询,prisma API 也有支持: working-with-json-fields

事务性

每个基本的 CURD 操作都会在单独的事务中运行(打印 sql 的时候可以看到包装有BEGIN...COMMIT),此外,还可以明确使用 $transaction 函数包装多个操作在一个事务中:

const [posts, totalPosts] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: "prisma" } } }),
  prisma.post.count(),
]);

调用 sql

prisma 提供了一些包含 Raw 关键字的函数,可以直接使用 sql,如:

const result = await prisma.$queryRaw`SELECT * FROM User`;
const result: number =
  await prisma.$executeRaw`UPDATE User SET active = true WHERE emailValidated = true`;

所有直接调用 sql 的函数可以参阅官方文档: raw-database-access

扩展阅读

觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :

付费文章

友情链接


相关文章