Prisma基础

# ORM

在和数据库进行交互时都需要使用特定的语言,比如使用关系型数据库时候的SQL,或者是使用MongoDB时的MQL。这些特定语言可以直接和数据库交互,快速进行增删改查。但是这些特定语言和在实际开发中使用的编程语言特性存在一定差距,并且很多时候可能都需要编写重复的代码。基于此我们可以想象,能否有一种工具让开发者可以直接使用其熟悉的编程语言,由该工具将代码转换为对应的SQL语句去操作数据库。一来这样可以避免直接使用SQL,直接使用原有编程语言即可实现数据库操作;二来能够减少一些重复代码,直接调用工具提供的API。

这样的工具被称为ORM(对象关系映射),用于在数据库和编程语言之间建立映射关系。ORM框架允许开发人员使用面向对象的方式来操作数据库,将数据库表映射为对象,将表中的行映射为对象的属性。ORM框架提供了一种抽象层,隐藏了底层数据库的细节,使开发人员可以使用面向对象的方式来进行数据库操作,而不必直接编写SQL语句。

Prisma就是一种ORM工具,为Node.js和TypeScript设计,方便不熟悉其他语言的前端开发者学习使用。

# 定义模型

按照Prisma官方文档 (opens new window)的快速开始章节的操作步骤,能够在本地快速启动一个Prisma项目。在schema.prisma文件内部就能开始定义模型用于和数据库进行交互。首先是原版的文件

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

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

generator用于使用Prisma时相关的API。datasoure这里告诉Prisma使用的数据库的类型以及数据库的url链接,一般将url链接定义在env文件中保证安全性。之后就可以在这个文件中定义模型了。

先定义两个简单的模型:

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

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

如果你熟悉SQL,很快就能发现model定义的方式非常类似在数据库中定义table的方式。包含字段名称,字段类型以及修饰符等等。实际上二者的作用确实基本一致,model在Prisma中的作用就是用于代替table,同时提供一些更加简单的操作。

定义完model之后,还需要将其应用到数据库,Prisma和我们的数据库是相互独立的,执行以下命令可将model应用于数据库:

npx prisma migrate dev --name init

为了实现操作数据库以及后续开发者能够使用Prisma提供的简单的方式继续操作数据库,这个命令主要进行了一下三个步骤:

  1. 创建了一个SQL迁移文件,文件位于prisma/migrations目录下。
  2. 执行这个SQL迁移文件,操作数据库,将model定义的规则应用于数据库。
  3. 底层执行prisma migration命令,安装Prisma客户端同时基于用于定义的模型生成对应的API。

想要和数据库交互,最后还是需要回到SQL。只是Prisma提供了一个更简单的方式操作数据库,我们只需要使用Prisma提供的API,最终生成SQL并执行的操作由Prisma代为进行。

# 操作数据

定义完模型之后,就可以使用Prisma提供的API直接对数据库进行操作了。在根目录下创建一个script.ts文件,写入以下内容:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  const user = await prisma.user.create({
    data: {
      name: 'Alice',
      email: 'alice@prisma.io',
    },
  })
  console.log(user)
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

运行这段代码就能发现我们已经在数据库中创建了一个新的用户,使用是prisma.user.create这样简单直接、语义化的API而非原生的SQL语句。Prisma还提供了一系列用于常规数据库操作的API,详细的可以参考g官方的API文档 (opens new window)

# 读取数据

Prisma Client提供了多种从数据库读取数据的方式,首先看最简单的获取所有数据

async function main() {
    const user = await prisma.user.findMany()
    console.log(user)
}

# 模型间关系

Prisma的一个非常重要的优势就是,能够非常简单的处理数据之间的“关系”。数据库中的表通常来说都会存在一些关联,这也是关系型数据库得到广泛应用的原因之一。 在上文中我们定义的UserPost这两个模型其实已经建立了联系。可以发现,每一个User都会包含一个Post,两者关联的依据则是,Post中的authorId要和User中的id相同。这一点体现在Post模型中author字段之后的修饰符。 这是一个非常简单的关联关系。下面介绍三个常见的模型间关系以及使用Prisma处理这三种关系的方式。

# 一对多

上面例子就是一个基本的一对多示例。在类似百度贴吧这样的网站中,一个用户可以发布很多个帖子。“一”就是用户,也就是User定义的模型,“多”就是帖子,也即Post定义的模型,这也是为什么在User的模型中Post需要加入数组修饰。

顺带一提,除了自增id,还可以使用uuid来作为每个模型id的修饰词,它将创建为每一个数据创建独一无二的id。现在修改原模型

model User {
  id    Int     @id @default(uuid())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(uuid())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

一对多关系中还有一个常见的点,就是处理对同一个模型的多次引用的时候,需要加入标识符来指明引用字段。在User模型中新加入两个字段:

  writtenPosts  Post[]
  favoratePosts Post[]

然后在POST中定义对应的关联

  author        User    @relation(fields: [authorId], references: [id]) 
  authorId      String
  favorateBy    User    @relation(fields: [favorateById], references: [id])
  favorateById  String

这样定义的方式会出现错误,因为Prisma不知道User中引用的两个Post到底指向Post模型中的哪一个字段。需要加入标识符来进行区分,修改后的字段定义如下:

  writtenPosts  Post[] @relation("writtenPosts")
  favoratePosts Post[] @relation("favoratePosts")


  author        User    @relation("writtenPosts", fields: [authorId], references: [id])
  authorId      String
  favorateBy    User    @relation("favoratePosts", fields: [favorateById], references: [id])
  favorateById  String

有了标识符就能知道具体建立关系时使用的字段。

# 多对多

多对多关系常见于类别。比如一个帖子可以拥有多种类别,而一个类别中则可以包含多个帖子。先来定义一个Category模型。

model Category {
  id    String @id @default(uuid())
  posts Post[]
}

回到Post模型中,加入一个字段:

  category Category[]

这样就建立了一个多对多的关系。这个工作简洁到我们只需要在两个模型中各自定义一个对另一个模型的多次引用,Prisma就能自动建立起一个多对多的关系表。 之后,用户创建的每一个帖子都可以拥有多个类别,而每个类别中也可以包含许多不同的帖子。

# 一对一

一对一关系的定义则更加简单,我们再另外定义一个模型

model UserReference {
  id           String  @id @default(uuid())
  emailUpdates Boolean @default(false)
  user         User    @relation(fields: [userId], references: [id])
  userId       Int     @unique
}

同样的,在User中新增一个字段

  userReference UserReference?

这样就建立了一个一对一的关系,注意这里的@unique,它确保这个userId对于每个UserReference来说都是唯一的,这也是一对一关系的基础。

# 模型属性

在字段名称以及字段数据类型之后的,一@开头的称为模型属性。可以用于设置字段默认值、标记不同模型字段之间的关系。除了之前提到的@id用于定义每个模型的ID,@default用于定义每个字段的默认值外。还有@unique这样的属性,确保该字段在每一个数据项当中都是独一无二的。还有一个用于数据更新的属性@updatedAt用于在数据更新记录更新的时间。

上面这类直接定义在字段之后的属性只会对该字段生效。Prisma还提供了另外一种属性的定义方式,块级属性。块级属性在模型中单独起一行定义,并且以@@开头。例如:

model User {
  name         String
  age          Int
  email        String
  emailUpdates Boolean @default(false)
  user         User    @relation(fields: [userId], references: [id])
  userId       Int     @unique

  @@unique([age, name])
  @@index(email)
  @@id([name, userId])

}

分别解释一下三个块级属性的作用。

  • 第一个的意思,两个用户的年龄和名字不能同时相同。
  • 第二个是用于给字段建立索引,这在快速查询和对数据进行排序的时候非常有用。
  • 最后一个是定义组合式的id。可以发现这个模型并没有定义id字段,而是使用@@id这样的方式定义了id。

# 枚举

枚举用于将一个字段的值限定在几个特定的值之内。比如一个用户在系统中可以拥有的角色包括”基础用户“,”管理者“,”已登录“三个。则可以按照如下方式定义。首先定义一个Role

enum Role {
  BASIC
  EDITOR
  ADMIN
}

然后在用户模型中添加一个字段表示用户角色,并将其类型设置设置为Role

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

枚举可以保证User模型中role字段的值只会是BASICADMINEDITOR三个中的一个。

上次更新:: 2023/12/25 15:10:30