数据建模
什么是数据建模?
术语 *数据建模* 指的是 **定义应用程序中对象的形状和结构的过程**,这些对象通常被称为“应用程序模型”。在关系数据库(如 PostgreSQL)中,它们存储在 *表* 中。当使用文档数据库(如 MongoDB)时,它们存储在 *集合* 中。
根据你的应用程序的领域,模型会有所不同。例如,如果你正在编写一个博客应用程序,你可能有诸如 *博客*、*作者*、*文章* 等模型。在编写一个汽车共享应用程序时,你可能拥有诸如 *司机*、*汽车*、*路线* 等模型。应用程序模型使你能够在代码中通过创建相应的 *数据结构* 来表示这些不同的实体。
在进行数据建模时,你通常会问这样的问题:
- 我的应用程序中的主要实体/概念是什么?
- 它们之间是如何关联的?
- 它们的主要特征/属性是什么?
- 如何使用我的技术栈来表示它们?
不使用 Prisma ORM 的数据建模
数据建模通常需要在(至少)两个层面进行:
- 在 **数据库** 层面
- 在 **应用程序** 层面(即,在你的编程语言中)
应用程序模型在这两个层面上的表示方式可能会因以下几个原因而有所不同:
- 数据库和编程语言使用不同的数据类型
- 关系在数据库中的表示方式与在编程语言中不同
- 数据库通常具有更强大的数据建模功能,例如索引、级联删除或各种附加约束(例如,唯一、非空等)
- 数据库和编程语言具有不同的技术约束
数据库层面的数据建模
关系数据库
在关系数据库中,模型由 *表* 表示。例如,你可能会定义一个 `users` 表来存储有关你的应用程序用户的信息。使用 PostgreSQL,你可以按如下方式定义它:
CREATE TABLE users (
user_id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(255),
email VARCHAR(255) UNIQUE NOT NULL,
isAdmin BOOLEAN NOT NULL DEFAULT false
);
具有一些随机数据的 `users` 表的可视化表示可能如下所示:
user_id | name | email | isAdmin |
---|---|---|---|
1 | Alice | [email protected] | false |
2 | Bob | [email protected] | false |
3 | Sarah | [email protected] | true |
它具有以下列:
user_id
:一个整数,它随着 `users` 表中每个新记录而递增。它也代表每个记录的 主键。name
:一个最多包含 255 个字符的字符串。email
:一个最多包含 255 个字符的字符串。此外,添加的约束表示对于 `email` 列,任何两个记录都不能具有重复值,并且 *每个* 记录都需要为此列设置一个值。isAdmin
:一个布尔值,指示用户是否具有管理员权限(默认值:`false`)
MongoDB
在 MongoDB 数据库中,模型由 *集合* 表示,并且包含可以具有任何结构的 *文档*
{
_id: '607ee94800bbe41f001fd568',
slug: 'prisma-loves-mongodb',
title: 'Prisma <3 MongoDB',
body: "This is my first post. Isn't MongoDB + Prisma awesome?!"
}
Prisma Client 当前期望一个一致的模型和 规范化的模型设计。这意味着:
- 如果模型或字段未在 Prisma schema 中出现,则会被忽略
- 如果字段是必需的但在 MongoDB 数据集中不存在,你将收到错误
应用程序层面的数据建模
除了创建表示你的应用程序域中实体的表之外,你还需要在你的编程语言中创建应用程序模型。在面向对象的语言中,这通常通过创建 *类* 来表示你的模型来完成。根据编程语言的不同,这也可以使用 *接口* 或 *结构体* 来完成。
你的数据库中的表和你代码中定义的模型之间通常存在很强的关联性。例如,为了表示你的应用程序中上述 `users` 表中的记录,你可能会定义一个类似于这样的 JavaScript (ES6) 类:
class User {
constructor(user_id, name, email, isAdmin) {
this.user_id = user_id
this.name = name
this.email = email
this.isAdmin = isAdmin
}
}
当使用 TypeScript 时,你可能会定义一个接口来代替
interface User {
user_id: number
name: string
email: string
isAdmin: boolean
}
请注意,在这两种情况下,`User` 模型都具有与前面示例中 `users` 表相同的属性。虽然数据库表和应用程序模型之间通常存在 1:1 的映射,但模型在数据库和你的应用程序中以完全不同的方式表示也是有可能的。
通过这种设置,你可以从 `users` 表中检索记录并将它们存储为你的 `User` 类型的实例。以下示例代码片段使用 `pg` 作为 PostgreSQL 的驱动程序,并基于上面定义的 JavaScript 类创建一个 `User` 实例
const resultRows = await client.query('SELECT * FROM users WHERE user_id = 1')
const userData = resultRows[0]
const user = new User(
userData.user_id,
userData.name,
userData.email,
userData.isAdmin
)
// user = {
// user_id: 1,
// name: "Alice",
// email: "[email protected]",
// isAdmin: false
// }
请注意,在这些示例中,应用程序模型是“哑的”,这意味着它们不实现任何逻辑,但它们的唯一目的是将数据作为 *普通的旧 JavaScript 对象* 来携带。
使用 ORM 进行数据建模
ORM 通常在面向对象的语言中使用,以使开发人员更容易使用数据库。ORM 的关键特征在于,它允许你根据 *类* 对你的应用程序数据进行建模,这些类被映射到底层数据库中的 *表*。
与上面解释的方法相比,主要区别在于这些类不仅携带数据,而且还实现了大量的逻辑。主要用于存储、检索、序列化和反序列化,但有时它们也实现特定于你的应用程序的业务逻辑。
这意味着,你不需要编写 SQL 语句来在数据库中读取和写入数据,而是你的模型类的实例提供了一个 API 来存储和检索数据。
Sequelize 是 Node.js 生态系统中流行的 ORM,以下是如何使用 Sequelize 的建模方法定义前面部分中相同的 `User` 模型:
class User extends Model {}
User.init(
{
user_id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: Sequelize.STRING(255),
email: {
type: Sequelize.STRING(255),
unique: true,
},
isAdmin: Sequelize.BOOLEAN,
},
{ sequelize, modelName: 'user' }
)
为了使这个 `User` 类的示例能够工作,你仍然需要在数据库中创建相应的表。使用 Sequelize,你有两种方法可以做到这一点:
- 运行 `User.sync()`(通常不建议用于生产环境)
- 使用 Sequelize 迁移 来更改你的数据库 schema
请注意,你永远不会像上一节中所示那样手动实例化 `User` 类(使用 `new User(...)`),而是调用 `User` 类上的 *静态* 方法,然后这些方法返回 `User` 模型实例
const user = await User.findByPk(42)
对 `findByPk` 的调用创建一个 SQL 语句来检索由 ID 值 `42` 标识的 `User` 记录。
生成的 `user` 对象是 Sequelize 的 `Model` 类的实例(因为 `User` 继承自 `Model`)。它不是 POJO,而是一个实现了来自 Sequelize 的附加行为的对象。
使用 Prisma ORM 进行数据建模
根据你想在你的应用程序中使用 Prisma ORM 的哪些部分,数据建模流程看起来略有不同。以下两节解释了仅使用 **Prisma Client** 和使用 **Prisma Client 和 Prisma Migrate** 的工作流程。
无论哪种方法,使用 Prisma ORM,你永远不会通过手动定义类、接口或结构体在你的编程语言中创建应用程序模型。相反,应用程序模型是在你的 Prisma schema 中定义的
- **仅 Prisma Client**:Prisma schema 中的应用程序模型是 *基于对你的数据库 schema 的内省生成的*。数据建模主要发生在数据库层面。
- **Prisma Client 和 Prisma Migrate**:数据建模发生在 Prisma schema 中,通过 *手动向其添加应用程序模型*。Prisma Migrate 将这些应用程序模型映射到底层数据库中的表(目前仅支持关系数据库)。
例如,前面示例中的 `User` 模型将在 Prisma schema 中表示如下:
model User {
user_id Int @id @default(autoincrement())
name String?
email String @unique
isAdmin Boolean @default(false)
}
一旦应用程序模型在你的 Prisma schema 中(无论它们是通过内省添加的还是你手动添加的),下一步通常是生成 Prisma Client,它提供了一个程序化和类型安全的 API,用于以你的应用程序模型的形状读取和写入数据。
Prisma Client 使用 TypeScript 类型别名 来表示你代码中的应用程序模型。例如,`User` 模型将在生成的 Prisma Client 库中表示如下:
export declare type User = {
id: number
name: string | null
email: string
isAdmin: boolean
}
除了生成的类型之外,Prisma Client 还提供了一个数据访问 API,你可以在安装 `@prisma/client` 包后使用它
import { PrismaClient } from '@prisma/client'
// or
// const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
// use inside an `async` function to `await` the result
await prisma.user.findUnique(...)
await prisma.user.findMany(...)
await prisma.user.create(...)
await prisma.user.update(...)
await prisma.user.delete(...)
await prisma.user.upsert(...)
仅使用 Prisma Client
当仅使用 Prisma Client 并且 *不* 在你的应用程序中使用 Prisma Migrate 时,数据建模需要通过 SQL 在数据库层面进行。一旦你的 SQL schema 准备就绪,你就可以使用 Prisma 的内省功能将应用程序模型添加到你的 Prisma schema 中。最后,你生成 Prisma Client,它会为你创建类型以及程序化 API,以便你在数据库中读取和写入数据。
这是主要工作流程的概述:
- 使用 SQL 更改你的数据库 schema(例如 `CREATE TABLE`、`ALTER TABLE` 等)
- 运行 `prisma db pull` 来内省数据库并将应用程序模型添加到 Prisma schema
- 运行 `prisma generate` 来更新你的 Prisma Client API
使用 Prisma Client 和 Prisma Migrate
当使用 Prisma Migrate 时,你在 Prisma schema 中定义你的应用程序模型,并且对于关系数据库,使用 `prisma migrate` 子命令来生成纯 SQL 迁移文件,你可以在应用之前编辑这些文件。对于 MongoDB,你使用 `prisma db push` 来代替,它直接将更改应用到你的数据库。
这是主要工作流程的概述:
- 手动更改你的 Prisma schema 中的应用程序模型(例如,添加一个新模型,删除一个现有模型,...)
- 运行 `prisma migrate dev` 来创建和应用迁移,或者运行 `prisma db push` 来直接应用更改(在这两种情况下,Prisma Client 都会自动生成)