乐趣区

关于javascript:精读Prisma-的使用

ORM(Object relational mappers) 的含意是,将数据模型与 Object 建设强力的映射关系,这样咱们对数据的增删改查能够转换为操作 Object(对象)。

Prisma 是一个古代 Nodejs ORM 库,依据 Prisma 官网文档 能够理解这个库是如何设计与应用的。

概述

Prisma 提供了大量工具,包含 Prisma Schema、Prisma Client、Prisma Migrate、Prisma CLI、Prisma Studio 等,其中最外围的两个是 Prisma Schema 与 Prisma Client,别离是形容利用数据模型与 Node 操作 API。

与个别 ORM 齐全由 Class 形容数据模型不同,Primsa 采纳了一个全新语法 Primsa Schema 形容数据模型,再执行 prisma generate 产生一个配置文件存储在 node_modules/.prisma/client 中,Node 代码里就能够应用 Prisma Client 对数据增删改查了。

Prisma Schema

Primsa Schema 是在最大水平贴近数据库构造形容的根底上,对关联关系进行了进一步形象,并且背地保护了与数据模型的对应关系,下图很好的阐明了这一点:

<img width=400 src=”https://z3.ax1x.com/2021/10/17/5YwZoF.png”>

能够看到,简直与数据库的定义截然不同,惟一多进去的 postsauthor 其实是补救了数据库表关联外键中不直观的局部,将这些外键转化为实体对象,让操作时感触不到外键或者多表的存在,在具体操作时再转化为 join 操作。上面是对应的 Prisma Schema:

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

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

model Post {id        Int     @id @default(autoincrement())
  title     String
  content   String? @map("post_content")
  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[]}

datasource db 申明了链接数据库信息;generator client 申明了应用 Prisma Client 进行客户端操作,也就是说 Prisma Client 其实是能够替换实现的;model 是最外围的模型定义。

在模型定义中,能够通过 @map 批改字段名映射、@@map 批改表名映射,默认状况下,字段名与 key 名雷同:

model Comment {title @map("comment_title")

  @@map("comments")
}

字段由上面四种形容组成:

  • 字段名。
  • 字段类型。
  • 可选的类型润饰。
  • 可选的属性形容。
model Tag {name String? @id}

在这个形容里,蕴含字段名 name、字段类型 String、类型润饰 ?、属性形容 @id

字段类型

字段类型能够是 model,比方关联类型字段场景:

model Post {id       Int       @id @default(autoincrement())
  // Other fields
  comments Comment[] // A post can have many comments}

model Comment {
  id     Int
  // Other fields
  Post   Post? @relation(fields: [postId], references: [id]) // A comment can have one post
  postId Int?
}

关联场景有 1v1, nv1, 1vn, nvn 四种状况,字段类型能够为定义的 model 名称,并应用属性形容 @relation 定义关联关系,比方下面的例子,形容了 CommenctPost 存在 nv1 关系,并且 Comment.postIdPost.id 关联。

字段类型还能够是底层数据类型,通过 @db. 形容,比方:

model Post {id @db.TinyInt(1)
}

对于 Prisma 不反对的类型,还能够应用 Unsupported 润饰:

model Post {someField Unsupported("polygon")?
}

这种类型的字段无奈通过 ORM API 查问,但能够通过 queryRaw 形式查问。queryRaw 是一种 ORM 对原始 SQL 模式的反对,在 Prisma Client 会提到。

类型润饰

类型润饰有 ? [] 两种语法,比方:

model User {
  name  String?
  posts Post[]}

别离示意可选与数组。

属性形容

属性形容有如下几种语法:

model User {id        Int     @id @default(autoincrement())
  isAdmin   Boolean @default(false)
  email     String  @unique

  @@unique([firstName, lastName])
}

@id 对应数据库的 PRIMARY KEY。

@default 设置字段默认值,能够联结函数应用,比方 @default(autoincrement()),可用函数包含 autoincrement()dbgenerated()cuid()uuid()now(),还能够通过 dbgenerated 间接调用数据库底层的函数,比方 dbgenerated("gen_random_uuid()")

@unique 设置字段值惟一。

@relation 设置关联,下面曾经提到过了。

@map 设置映射,下面也提到过了。

@updatedAt 修饰字段用来存储上次更新工夫,个别是数据库自带的能力。

@ignore 对 Prisma 标记有效的字段。

所有属性形容都能够组合应用,并且还存在需对 model 级别的形容,个别用两个 @ 形容,包含 @@id@@unique@@index@@map@@ignore

ManyToMany

Prisma 在多对多关联关系的形容上也下了功夫,反对隐式关联形容:

model Post {id         Int        @id @default(autoincrement())
  categories Category[]}

model Category {id    Int    @id @default(autoincrement())
  posts Post[]}

看上去很天然,但其实背地暗藏了不少实现。数据库多对多关系个别通过第三张表实现,第三张表会存储两张表之间外键对应关系,所以如果要显式定义其实是这样的:

model Post {id         Int                 @id @default(autoincrement())
  categories CategoriesOnPosts[]}

model Category {id    Int                 @id @default(autoincrement())
  posts CategoriesOnPosts[]}

model CategoriesOnPosts {post       Post     @relation(fields: [postId], references: [id])
  postId     Int // relation scalar field (used in the `@relation` attribute above)
  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int // relation scalar field (used in the `@relation` attribute above)
  assignedAt DateTime @default(now())
  assignedBy String

  @@id([postId, categoryId])
}

背地生成如下 SQL:

CREATE TABLE "Category" (id SERIAL PRIMARY KEY);
CREATE TABLE "Post" (id SERIAL PRIMARY KEY);
-- Relation table + indexes -------------------------------------------------------
CREATE TABLE "CategoryToPost" (
    "categoryId" integer NOT NULL,
    "postId" integer NOT NULL,
    "assignedBy" text NOT NULL
    "assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY ("categoryId")  REFERENCES "Category"(id),
    FOREIGN KEY ("postId") REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);

Prisma Client

形容好 Prisma Model 后,执行 prisma generate,再利用 npm install @prisma/client 装置好 Node 包后,就能够在代码里操作 ORM 了:

import {PrismaClient} from '@prisma/client'

const prisma = new PrismaClient()

CRUD

应用 create 创立一条记录:

const user = await prisma.user.create({
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
  },
})

应用 createMany 创立多条记录:

const createMany = await prisma.user.createMany({
  data: [{ name: 'Bob', email: 'bob@prisma.io'},
    {name: 'Bobo', email: 'bob@prisma.io'}, // Duplicate unique key!
    {name: 'Yewande', email: 'yewande@prisma.io'},
    {name: 'Angelique', email: 'angelique@prisma.io'},
  ],
  skipDuplicates: true, // Skip 'Bobo'
})

应用 findUnique 查找单条记录:

const user = await prisma.user.findUnique({
  where: {email: 'elsa@prisma.io',},
})

对于联结索引的状况:

model TimePeriod {
  year    Int
  quarter Int
  total   Decimal

  @@id([year, quarter])
}

须要再嵌套一层由 _ 拼接的 key:

const timePeriod = await prisma.timePeriod.findUnique({
  where: {
    year_quarter: {
      quarter: 4,
      year: 2020,
    },
  },
})

应用 findMany 查问多条记录:

const users = await prisma.user.findMany()

能够应用 SQL 中各种条件语句,语法如下:

const users = await prisma.user.findMany({
  where: {role: 'ADMIN',},
  include: {posts: true,},
})

应用 update 更新记录:

const updateUser = await prisma.user.update({
  where: {email: 'viola@prisma.io',},
  data: {name: 'Viola the Magnificent',},
})

应用 updateMany 更新多条记录:

const updateUsers = await prisma.user.updateMany({
  where: {
    email: {contains: 'prisma.io',},
  },
  data: {role: 'ADMIN',},
})

应用 delete 删除记录:

const deleteUser = await prisma.user.delete({
  where: {email: 'bert@prisma.io',},
})

应用 deleteMany 删除多条记录:

const deleteUsers = await prisma.user.deleteMany({
  where: {
    email: {contains: 'prisma.io',},
  },
})

应用 include 示意关联查问是否失效,比方:

const getUser = await prisma.user.findUnique({
  where: {id: 19,},
  include: {posts: true,},
})

这样就会在查问 user 表时,顺带查问所有关联的 post 表。关联查问也反对嵌套:

const user = await prisma.user.findMany({
  include: {
    posts: {
      include: {categories: true,},
    },
  },
})

筛选条件反对 equalsnotinnotInltltegtgtecontainssearchmodestartsWithendsWithANDORNOT,个别用法如下:

const result = await prisma.user.findMany({
  where: {
    name: {equals: 'Eleanor',},
  },
})

这个语句代替 sql 的 where name="Eleanor",即通过对象嵌套的形式表白语义。

Prisma 也能够间接写原生 SQL:

const email = 'emelie@prisma.io'
const result = await prisma.$queryRaw(Prisma.sql`SELECT * FROM User WHERE email = ${email}`
)

中间件

Prisma 反对中间件的形式在执行过程中进行拓展,看上面的例子:

const prisma = new PrismaClient()

// Middleware 1
prisma.$use(async (params, next) => {console.log(params.args.data.title)
  console.log('1')
  const result = await next(params)
  console.log('6')
  return result
})

// Middleware 2
prisma.$use(async (params, next) => {console.log('2')
  const result = await next(params)
  console.log('5')
  return result
})

// Middleware 3
prisma.$use(async (params, next) => {console.log('3')
  const result = await next(params)
  console.log('4')
  return result
})

const create = await prisma.post.create({
  data: {title: 'Welcome to Prisma Day 2020',},
})

const create2 = await prisma.post.create({
  data: {title: 'How to Prisma!',},
})

输入如下:

Welcome to Prisma Day 2020 
1 
2 
3 
4 
5 
6 
How to Prisma! 
1 
2 
3 
4 
5 
6

能够看到,中间件执行程序是洋葱模型,并且每个操作都会触发。咱们能够利用中间件拓展业务逻辑或者进行操作工夫的打点记录。

精读

ORM 的两种设计模式

ORM 有 Active Record 与 Data Mapper 两种设计模式,其中 Active Record 使对象背地齐全对应 sql 查问,当初曾经不怎么风行了,而 Data Mapper 模式中的对象并不知道数据库的存在,即两头多了一层映射,甚至背地不须要对应数据库,所以能够做一些很轻量的调试性能。

Prisma 采纳了 Data Mapper 模式。

ORM 容易引发性能问题

当数据量大,或者性能、资源敏感的状况下,咱们须要对 SQL 进行优化,甚至咱们须要对特定的 Mysql 的特定版本的某些内核谬误,对 SQL 进行某些看似无意义的申明调优(比方在 where 之前再进行雷同条件的 IN 范畴限定),有的时候能获得惊人的性能晋升。

而 ORM 是建设在一个较为理想化实践根底上的,即数据模型能够很好的转化为对象操作,然而对象操作因为屏蔽了细节,咱们无奈对 SQL 进行针对性调优。

另外,得益于对象操作的便利性,咱们很容易通过 obj.obj. 的形式拜访某些属性,但这背地生成的却是一系列未经优化(或者局部主动优化)的简单 join sql,咱们在写这些 sql 时会提前思考性能因素,但通过对象调用时却因为成本低,或感觉 ORM 有 magic 优化等想法,写出很多实际上不合理的 sql。

Prisma Schema 的益处

其实从语法上,Prisma Schema 与 Typeorm 基于 Class + 装璜器的拓展简直能够等价转换,但 Prisma Schema 在理论应用中有一个很不错的劣势,即缩小样板代码以及稳固数据库模型。

缩小样板代码比拟好了解,因为 Prisma Schema 并不会呈现在代码中,而稳固模型是指,只有不执行 prisma generate,数据模型就不会变动,而且 Prisma Schema 也独立于 Node 存在,甚至能够不放在我的项目源码中,相比之下,批改起来会更加谨慎,而齐全用 Node 定义的模型因为自身是代码的一部分,可能会忽然被批改,而且也没有执行数据库构造同步的操作。

如果我的项目采纳 Prisma,则模型变更后,能够执行 prisma db pull 更新数据库构造,再执行 prisma generate 更新客户端 API,这个流程比拟清晰。

总结

Prisma Schema 是 Prisma 的一大特色,因为这部分形容独立于代码,带来了如下几个益处:

  1. 定义比 Node Class 更简洁。
  2. 不生成冗余的代码构造。
  3. Prisma Client 更加轻量,且查问返回的都是 Pure Object。

至于 Prisma Client 的 API 设计其实并没有特地突出之处,无论与 sequelize 还是 typeorm 的 API 设计相比,都没有太大的优化,只是格调不同。

不过对于记录的创立,我更喜爱 Prisma 的 API:

// typeorm - save API
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)

// typeorm - insert API
const userRepository = getManager().getRepository(User)
userRepository.insert({name: 'Alice',})

// sequelize
const user = User.build({name: 'Alice',})
await user.save()

// Mongoose
const user = await User.create({
  name: 'Alice',
  email: 'alice@prisma.io',
})

// prisma
const newUser = await prisma.user.create({
  data: {name: 'Alice',},
})

首先存在 prisma 这个顶层变量,应用起来会十分不便,另外从 API 拓展上来说,尽管 Mongoose 设计得更简洁,但增加一些条件时拓展性会有余,导致构造不太稳固,不利于对立记忆。

Prisma Client 的 API 对立采纳上面这种构造:

await prisma.modelName.operateName({
  // 数据,比方 create、update 时会用到
  data: /** ... */,
  // 条件,大部分状况都能够用到
  where: /** ... */,
  // 其它非凡参数,或者 operater 特有的参数
})

所以总的来说,Prisma 尽管没有对 ORM 做出革命性扭转,但在微翻新与 API 优化上都做得足够棒,github 更新也比拟沉闷,如果你决定应用 ORM 开发我的项目,还是比拟举荐 Prisma 的。

在理论应用中,为了躲避 ORM 产生蠢笨 sql 导致的性能问题,能够利用 Prisma Middleware 监控查问性能,并对性能较差的中央采纳 prisma.$queryRaw 原生 sql 查问。

探讨地址是:精读《Prisma 的应用》· Issue #362 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

退出移动版