Prisma + Mysql 不使用外键的方法
“你在关系数据库中使用外键吗?” 这是一个开发者群体中另一个“该使用空格还是 TAB 进行缩进?”的经典问题。
“你在关系数据库中使用外键吗?”
这个问题引发的争论从未断过,但也从未有定论。网络上这个问题的讨论 Google 一下,几乎每年都有,以下是一些观点:
[为什么数据库不应该使用外键](https://draveness.me/whys-the-design-database-foreign-key/)
[关于外键,为什么国内基本都不推荐使用,国外基本都推荐使用?](https://www.v2ex.com/t/799876)
[外键应不应该建立](https://www.v2ex.com/t/673284)
[MySQL数据库应不应该拿掉所有的外键约束?](https://segmentfault.com/q/1010000000401455)
在这些讨论中,有人提了这么个问题:
“如果不用外键,框架自带的 ORM 模型就发挥不了作用吗?”
刚好最近在学习 JS 的 ORM 框架 Prisma,就用它试了一下这个场景。
Prisma 的基本使用流程
Prisma 官网:[https://www.prisma.io/](https://www.prisma.io/)
prisma 是一个开源的数据库工具链项目,号称是“下一代的 node.js 和 typescript ORM 框架”。
为了试一下不用外键,ORM 框架的模型是否能发挥作用,我们按 Prisma 官网的教程跑一遍代码。
安装 Prisma,初始化项目
首先创建目录,安装 Prisma:
// 创建目录
mkdir hello-prisma
// 进入目录
cd hello-prisma
// 初始化项目,安装 prisma
npm init -y
npm install prisma typescript ts-node @types/node --save-dev
安装完成后,初始化 Prisma 项目:
npx prisma init
这个命令创建了一个名为prisma
的新目录,其中包含一个名为schema.prisma
的文件和一个位于项目根目录中的.env
文件schema.prisma
包含prisma模式以及数据库连接和prisma客户端生成器。 .env
是一个dotenv用于定义环境变量的文件(用于数据库连接)。
Prisma 连接数据库
Prisma 初始化创建了一个项目的配置文件,路径为/prisma/schema.prisma
,打开这个文件,并修改为数据库类型和地址,连接本地数据库(提前在 Mysql 里创建一个空的数据库 mydb:
// **/prisma/schema.prisma**
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// /.env
DATABASE_URL="mysql://root:password@localhost:3306/mydb"
使用 Prisma Migrate 创建数据表
配置好数据库连接后,在/prisma/schema.prisma
文件里定义我们要在数据库创建的数据表(在 /prisma/schema.prisma
里配置 Prisma 数据模型)
// **/prisma/schema.prisma**
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
}
按官网的例子,上面的模型定义了三个表,分别是用户表 User
,用户资料表 Profile
,和用户帖子表 Post
,其中定义了两个外键,分别是 Post
表里的 authorId
和 Profile
表里的 userId
,它们都指向 User
表的 id
。
定义好数据模型后,要将数据模型映射到数据库架构,需要使用prisma migrate
CLI命令:
$npx prisma migrate dev --name init
// 注意:
因为在运行 prisma migrate dev 命令后,prisma 会接着调用 prisma generate 命令,这个命令会生成一个我们后续使用需要的模型定义文件。而根据我们的配置,prisma generate 命令运行时会检查 @prisma/client 是否安装,所以在运行上面的命令前需要安装:
npm install @prisma/client
这个命令根据上面定义好的数据模型,生成了一个 SQL 文件,并在数据库中执行了这个文件,从而达到在数据库创建数据表的目的。
我们执行上面的命令,现在数据库里就多了三个表: User
, Profile
, Post
。我们看下 prisma 生成的 SQL 文件:
CREATE TABLE "Post" (
"id" SERIAL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE "Profile" (
"id" SERIAL,
"bio" TEXT,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE "User" (
"id" SERIAL,
"email" TEXT NOT NULL,
"name" TEXT,
PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Profile.userId_unique" ON "Profile"("userId");
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
ALTER TABLE "Post" ADD FOREIGN KEY("authorId")REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Profile" ADD FOREIGN KEY("userId")REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
从上面的代码看,确实创建了两个外键。我们也可以通过数据库的 ER 图看到关联关系:
使用 Prisma ORM
创建好数据表后,就可以正常使用 Prisma 进行数据库相关的操作了。
创建 /index.ts 文件
// 导入并实例化 Prisma 客户端
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// 数据库操作 - 写入数据
await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@prisma.io',
posts: {
create: { title: 'Hello World' },
},
profile: {
create: { bio: 'I like turtles' },
},
},
})
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
现在用下面的命令运行代码:
npx ts-node index.ts
由于定义了数据表的关联关系(外键),因此我们使用上面的函数,一次性创建了一个用户(user),同时创建了用户的资料(profile)和用户的一个帖子(post)。
然后我们使用把上面 main() 函数里的代码改成下面的样子以测试删除刚刚创建的用户:
const deletedUser = await prisma.user.delete({
where: { id: 1 },
})
执行代码,出现了这样的提示:
由于外键的限制,我们无法通过上面的代码,单独删除这个 user 。
这说明我们通过 Prisma 成功的创建了带外键的数据表并使用这些表之间的关联关系方便的进行数据库的操作。
总结:
Prisma 的基本使用流程为:
1、在 schema.prisma 中定义数据模型;
2、通过 npx prisma migrate dev --name XXX 创建数据库迁移文件并在数据库执行以修改数据表;
3、通过 npx prisma generate 生成 PrismaClient 需要的模型文件;
4、在程序中导入 PrismaClient 并实例化,并通过这个实例对数据库进行操作。
创建不带外键的数据表
我们把上面 schema.prisma
的数据模型修改下,注释掉创建数据表关联关系的语句:
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
content String?
published Boolean @default(true)
authorId Int
// relations
// author User @relation(fields: [authorId], references: [id])
}
model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int @unique
// relations
// user User @relation(fields: [userId], references: [id])
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
// relations
// posts Post[]
// profile Profile?
}
然后使用 npx prisma migrate dev --name XXX
对数据表进行修改并清空数据库。
执行后数据库表之间的关联关系不见了,如图:
可是,由于没有定义外键,因此我们在上一个例子中通过一个函数方便的进行关联数据的录入的方法不可用了,如图:
看来这个担心也不是空穴来风,失去了这些关联,使用 ORM 的便捷性和灵活性大打折扣。
使用不带外键的数据库,同时在 Prisma 中定义数据关联
那么有没有办法解决上面的问题呢?
通过第一部分我们知道 Prisma 通过 prisma migrate 命令进行数据库的创建,然后通过 prisma generate 生成我们在程序中使用 prisma-client 所需要的数据库模型文件。
也就是说 Prisma-client 是通过 prisma generate 所生成的数据库模型文件来理解数据表的关系并执行相应的数据库操作的。那么,如果我们这样做会怎样呢:
1,创建不带外键的数据表
2,修改 schema.prisma 文件,加入关联关系的定义
3,手动运行 prisma generate 命令,生成 prisma-client 使用的模型文件
4,像有外键一样正常使用 Prisma
我们试试。
修改 schema.prisma 并手动 prisma generate
ok,没有报错。
测试是否可以插入关联数据
此时,数据表在数据库层面是没有定义外键的,但在程序里,prisma-client 的模型里是有描述数据表之间的关联关系的(数据库里没外键,程序里有虚拟外键)。我们在这种情况下运行上面的插入数据的程序:
如上,成功写入用户数据和关联数据!
通过这种方法,我们成功的在没有外键的数据库里以有外键的方式使用 Prisma ORM 框架。
One More Thing
额,别高兴太早,我们在这种情况下运行一下第一部分那个删除用户的程序:
成功删除用户!
因为没有数据库里实际上是没有外键约束的,所以这种情况下删除用户不会被数据库因为外键关系而阻止。也就是说通过上面介绍的方法,我们可以在数据库不设外键的情况下很好的使用 Prisma 操作数据库,但是需要在程序层面更加小心,加入更多约束逻辑,以防误操作。
全文结束
很好的文章!我也是这样做的,但是在数据库更新的时候,再使用npx prisma db pull则会覆盖我之前手动添加的relation,有什么方法吗
prisma 现在已经支持不使用数据库外键来创建关联了。启用 Prisma 最新版本的预览特性【Referential integrity】可以解决这个问题。具体文档参见:https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-integrity