从 Mongoose 迁移
本指南介绍了如何从 Mongoose 迁移到 Prisma ORM。它使用扩展版的 Mongoose Express 示例 作为 示例项目 来演示迁移步骤。您可以在 GitHub 上找到此指南中使用的示例。
您可以在 Prisma ORM 与 Mongoose 页面上了解 Prisma ORM 与 Mongoose 的比较方式。
迁移过程概述
请注意,从 Mongoose 迁移到 Prisma ORM 的步骤始终相同,无论您正在构建哪种应用程序或 API 层。
- 安装 Prisma CLI
- 内省您的数据库
- 安装并生成 Prisma 客户端
- 逐步用 Prisma 客户端替换您的 Mongoose 查询
无论您是在构建 REST API(例如使用 Express、koa 或 NestJS)、GraphQL API(例如使用 Apollo Server、TypeGraphQL 或 Nexus)还是任何其他使用 Mongoose 进行数据库访问的应用程序,这些步骤都适用。
Prisma ORM 非常适合 **增量采用**。这意味着,您无需一次性将整个项目从 Mongoose 迁移到 Prisma ORM,而是可以**逐步**将您的数据库查询从 Mongoose 迁移到 Prisma ORM。
示例项目概述
在本指南中,我们将使用一个用 Express 构建的 REST API 作为 示例项目 迁移到 Prisma ORM。它包含三个文档和一个子文档(嵌入式文档)。
- post.js
- user.js
- category.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const PostSchema = new Schema({
title: String,
content: String,
published: {
type: Boolean,
default: false,
},
author: {
type: Schema.Types.ObjectId,
ref: 'author',
required: true,
},
categories: [
{
type: Schema.Types.ObjectId,
ref: 'Category',
},
],
})
module.exports = mongoose.model('Post', PostSchema)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)
const UserSchema = new Schema({
name: String,
email: {
type: String,
unique: true,
},
profile: {
type: ProfileSchema,
default: () => ({}),
},
})
module.exports = mongoose.model('User', UserSchema)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const CategorySchema = new Schema({
name: {
type: String,
required: true,
},
})
module.exports = mongoose.model('Category', CategorySchema)
模型/文档具有以下类型的关系
- 1-n:
User
↔Post
- m-n:
Post
↔Category
- 子文档/嵌入式文档:
User
↔Profile
在本指南中使用的示例中,路由处理程序位于 src/controllers
目录中。模型位于 src/models
目录中。从那里,模型被拉入一个中心 src/routes.js
文件,该文件用于在 src/index.js
中定义所需的路由。
└── blog-mongoose
├── package.json
└──src
├── controllers
│ ├── post.js
│ └── user.js
├── models
│ ├── category.js
│ ├── post.js
│ └── user.js
├── index.js
├── routes.js
└── seed.js
示例存储库在 package.json
文件中包含一个 seed
脚本。
运行 npm run seed
以使用 ./src/seed.js
文件中的示例数据填充您的数据库。
步骤 1. 安装 Prisma CLI
采用 Prisma ORM 的第一步是在您的项目中 安装 Prisma CLI
npm install prisma --save-dev
步骤 2. 内省您的数据库
内省是检查数据库结构的过程,在 Prisma ORM 中使用它来生成 数据模型 到您的 Prisma 架构 中。
2.1. 设置 Prisma
在内省您的数据库之前,您需要设置您的 Prisma 架构 并将 Prisma ORM 连接到您的数据库。在您的终端中运行以下命令以创建一个基本的 Prisma 架构文件。
npx prisma init --datasource-provider mongodb
此命令创建
- 一个名为
prisma
的新目录,其中包含一个schema.prisma
文件;您的 Prisma 架构指定了您的数据库连接和模型。 .env
: 一个dotenv
文件,位于您项目的根目录(如果它还不存在),用于配置您的数据库连接 URL 作为环境变量。
Prisma 架构目前如下所示
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
为了在使用 Prisma ORM 时获得最佳的开发体验,请参考 编辑器设置,了解语法高亮、格式化、自动完成以及更多酷炫的功能。
2.2. 连接您的数据库
在 .env
文件中配置您的 数据库连接 URL。
Mongoose 使用的连接 URL 格式类似于 Prisma ORM 使用的格式。
DATABASE_URL="mongodb://alice:myPassword43@localhost:27017/blog-mongoose"
有关更多详细信息,请参考 MongoDB 连接 URL 规范。
2.3. 运行 Prisma ORM 的内省
连接 URL 就位后,您可以 内省 您的数据库以生成 Prisma 模型
注意: MongoDB 是一个无模式数据库。为了在您的项目中增量采用 Prisma ORM,请确保您的数据库已填充了示例数据。Prisma ORM 通过对存储的数据进行采样并从数据库中的数据推断模式来内省 MongoDB 模式。
npx prisma db pull
这将创建以下 Prisma 模型
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
author String @db.ObjectId
categories String[] @db.ObjectId
content String
published Boolean
title String
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
}
生成的 Prisma 模型表示 MongoDB 集合,是您程序化 Prisma 客户端 API 的基础,它允许您向您的数据库发送查询。
2.4. 更新关系
MongoDB 不支持不同集合之间的关系。但是,您可以使用 ObjectId
字段类型或从一个文档到多个文档,使用集合中 ObjectId
的数组,在文档之间创建引用。引用将存储相关文档的 id(s)。您可以使用 Mongoose 提供的 populate()
方法,将引用填充为相关文档的数据。
按如下方式更新 Post
<-> User
之间的 1-n 关系
- 将
posts
模型中现有的author
引用重命名为authorId
并添加@map("author")
属性 - 在
posts
模型中添加author
关系字段,以及它的@relation
属性,指定fields
和references
- 在
users
模型中添加posts
关系
- diff
- schema.prisma
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
- author String @db.ObjectId
+ author users @relation(fields: [authorId], references: [id])
+ authorId String @map("author") @db.ObjectId
categories String[] @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
+ posts posts[]
}
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories String[] @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}
按如下方式更新 Post
<-> Category
之间的 m-n 引用
- 将
posts
模型中的categories
字段重命名为categoryIds
并使用@map("categories")
映射它 - 在
posts
模型中添加一个新的categories
关系字段 - 在
categories
模型中添加postIds
标量列表字段 - 在
categories
模型中添加posts
关系 - 在两个模型上都添加一个 关系标量
- 在两边添加
@relation
属性,指定fields
和references
参数
- diff
- schema.prisma
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
+ posts posts[] @relation(fields: [postIds], references: [id])
+ postIds String[] @db.ObjectId
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
- categories String[] @db.ObjectId
+ categories categories[] @relation(fields: [categoryIds], references: [id])
+ categoryIds String[] @map("categories") @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @map("__v")
posts posts[] @relation(fields: [postIds], references: [id])
postIds String[] @db.ObjectId
}
model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")
author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories categories[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId
}
model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}
2.5. 调整 Prisma 架构(可选)
通过内省生成的模型目前与您的数据库集合完全匹配。在本节中,您将了解如何调整 Prisma 模型的命名以符合 Prisma ORM 的命名约定。
这些调整中的一部分完全是可选的,如果您现在不想进行任何调整,可以随意跳到下一步。您可以在稍后的任何时间点返回并进行调整。
与 Prisma 模型当前的 snake_case 表示法相反,Prisma ORM 的命名约定是
- PascalCase 用于模型名称
- camelCase 用于字段名称
您可以通过 映射 Prisma 模型和字段名称到基础数据库中现有的表和列名称来调整命名,分别使用 @@map
和 @map
。
您可以使用 重命名符号 操作通过突出显示模型名称、按下 F2 键并最后键入所需的名称来重构模型名称。这将重命名它被引用的所有实例,并将 @@map()
属性添加到具有其先前名称的现有模型中。
如果您的模式包含 versionKey
,请通过在 v
字段中添加 @default(0)
和 @ignore
属性来更新它。这意味着该字段将从生成的 Prisma Client 中排除,并且将具有默认值 0。Prisma ORM 不处理文档版本控制。
还要注意,您可以重命名 关系字段 以优化您稍后将用于向数据库发送查询的 Prisma Client API。例如,user
模型上的 post
字段是一个列表,因此该字段的更好名称是 posts
,以表明它是复数形式。
通过包括 @default
属性来更新 published
字段以定义该字段的默认值。
您也可以将 UserProfile
复合类型重命名为 Profile
。
这是解决这些问题的 Prisma 模式调整版本
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
type Profile {
bio String
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @default(0) @map("__v") @ignore
posts Post[] @relation(fields: [post_ids], references: [id])
post_ids String[] @db.ObjectId
@@map("categories")
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean @default(false)
v Int @default(0) @map("__v") @ignore
author User @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId
categories Category[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId
@@map("posts")
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @default(0) @map("__v") @ignore
email String @unique(map: "email_1")
name String
profile Profile?
posts Post[]
@@map("users")
}
步骤 3. 安装 Prisma Client
下一步,您可以在项目中安装 Prisma Client,以便您可以开始替换当前使用 Mongoose 进行的项目中的数据库查询
npm install @prisma/client
步骤 4. 将您的 Mongoose 查询替换为 Prisma Client
在本节中,我们将根据示例 REST API 项目中的示例路由,展示一些从 Mongoose 迁移到 Prisma Client 的示例查询。有关 Prisma Client API 与 Mongoose 不同的全面概述,请查看 Mongoose 和 Prisma API 比较 页面。
首先,要设置您将用于从各种路由处理程序发送数据库查询的 PrismaClient
实例,请在 src
目录中创建一个名为 prisma.js
的新文件
touch src/prisma.js
现在,实例化 PrismaClient
并将其从文件中导出,以便您稍后在路由处理程序中使用它
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
module.exports = prisma
我们控制器文件中的导入如下
const Post = require('../models/post')
const User = require('../models/user')
const Category = require('../models/category')
const Post = require('../models/post')
const User = require('../models/user')
在从 Mongoose 迁移到 Prisma 时,您将更新控制器导入
const prisma = require('../prisma')
const prisma = require('../prisma')
4.1. 替换 GET
请求中的查询
本指南中使用的示例 REST API 有四个接受 GET
请求的路由
/feed?searchString={searchString}&take={take}&skip={skip}
:返回所有已发布的帖子- 查询参数(可选)
searchString
:按title
或content
筛选帖子take
:指定列表中应返回多少个对象skip
:指定应跳过多少个返回的对象
- 查询参数(可选)
/post/:id
:返回特定帖子/authors
:返回作者列表
让我们深入研究实现这些请求的路由处理程序。
/feed
/feed
处理程序的实现方式如下
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query
const or =
searchString !== undefined
? {
$or: [
{ title: { $regex: searchString, $options: 'i' } },
{ content: { $regex: searchString, $options: 'i' } },
],
}
: {}
const feed = await Post.find(
{
...or,
published: true,
},
null,
{
skip,
batchSize: take,
}
)
.populate({ path: 'author', model: User })
.populate('categories')
return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}
请注意,每个返回的 Post
对象都包含与之关联的 author
和 category
的关系。使用 Mongoose,包括关系不是类型安全的。例如,如果您在检索到的关系中出现拼写错误,您的数据库查询只会在运行时失败——JavaScript 编译器在这里不提供任何安全性。
以下是使用 Prisma Client 实现相同路由处理程序的方式
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query
const or = searchString
? {
OR: [
{ title: { contains: searchString } },
{ content: { contains: searchString } },
],
}
: {}
const feed = await prisma.post.findMany({
where: {
published: true,
...or,
},
include: { author: true, categories: true },
take: Number(take) || undefined,
skip: Number(skip) || undefined,
})
return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}
请注意,Prisma Client 包括 author
关系的方式绝对是类型安全的。如果您尝试包括 Post
模型上不存在的关系,JavaScript 编译器将抛出错误。
/post/:id
/post/:id
处理程序的实现方式如下
const getPostById = async (req, res) => {
const { id } = req.params
try {
const post = await Post.findById(id)
.populate({ path: 'author', model: User })
.populate('categories')
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,路由处理程序的实现方式如下
const getPostById = async (req, res) => {
const { id } = req.params
try {
const post = await prisma.post.findUnique({
where: { id },
include: {
author: true,
category: true,
},
})
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
4.2. 替换 POST
请求中的查询
REST API 有三个接受 POST
请求的路由
/user
:创建一个新的User
记录/post
:创建一个新的User
记录/user/:id/profile
:为具有给定 ID 的User
记录创建一个新的Profile
记录
/user
/user
处理程序的实现方式如下
const createUser = async (req, res) => {
const { name, email } = req.body
try {
const user = await User.create({
name,
email,
})
return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,路由处理程序的实现方式如下
const createUser = async (req, res) => {
const { name, email } = req.body
try {
const user = await prisma.user.create({
data: {
name,
email,
},
})
return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
/post
/post
处理程序的实现方式如下
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body
try {
const author = await User.findOne({ email: authorEmail })
if (!author) return res.status(404).json({ message: 'Author not found' })
const draft = await Post.create({
title,
content,
author: author._id,
})
res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,路由处理程序的实现方式如下
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body
try {
const draft = await prisma.post.create({
data: {
title,
content,
author: {
connect: {
email: authorEmail,
},
},
},
})
res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}
请注意,Prisma Client 嵌套写入在这里保存了最初的查询,其中 User
记录首先通过其 email
检索。这是因为,使用 Prisma Client,您可以使用任何唯一属性在关系中连接记录。
/user/:id/profile
/user/:id/profile
处理程序的实现方式如下
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await User.findByIdAndUpdate(
id,
{
profile: {
bio,
},
},
{ new: true }
)
if (!user) return res.status(404).json({ message: 'Author not found' })
return res.status(200).json(user)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,路由处理程序的实现方式如下
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await prisma.user.update({
where: { id },
data: {
profile: {
bio,
},
},
})
if (!user) return res.status(404).json({ message: 'Author not found' })
return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}
或者,您可以使用 set
属性以如下方式更新嵌入式文档的值
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body
try {
const user = await prisma.user.update({
where: {
id,
},
data: {
profile: {
set: { bio },
},
},
})
return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}
4.3. 替换 PUT
请求中的查询
REST API 有两个接受 PUT
请求的路由
/post/:id/:categoryId
:将具有:id
的帖子添加到具有:categoryId
的类别中/post/:id
:将帖子的published
状态更新为 true。
让我们深入研究实现这些请求的路由处理程序。
/post/:id/:categoryId
/post/:id/:categoryId
处理程序的实现方式如下
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.params
try {
const category = await Category.findById(categoryId)
if (!category)
return res.status(404).json({ message: 'Category not found' })
const post = await Post.findByIdAndUpdate(
{ _id: id },
{
categories: [{ _id: categoryId }],
},
{ new: true }
)
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,处理程序的实现方式如下
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.query
try {
const post = await prisma.post.update({
where: {
id,
},
data: {
categories: {
connect: {
id: categoryId,
},
},
},
})
if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
console.log({ error })
return res.status(500).json(error)
}
}
/post/:id
/post/:id
处理程序的实现方式如下
const publishDraft = async (req, res) => {
const { id } = req.params
try {
const post = await Post.findByIdAndUpdate(
{ id },
{ published: true },
{ new: true }
)
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
使用 Prisma ORM,处理程序的实现方式如下
const publishDraft = async (req, res) => {
const { id } = req.params
try {
const post = await prisma.post.update({
where: { id },
data: { published: true },
})
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}
更多
嵌入式文档 _id
字段
默认情况下,Mongoose 会为每个文档和嵌入式文档分配一个 _id
字段。如果您希望为嵌入式文档禁用此选项,可以将 _id
选项设置为 false。
const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)
文档版本键
Mongoose 会在创建时为每个文档分配一个版本。您可以通过将模型的 versionKey
选项设置为 false 来禁用 Mongoose 对文档进行版本控制。除非您是高级用户,否则 不建议 禁用它。
const ProfileSchema = new Schema(
{
bio: String,
},
{
versionKey: false,
}
)
迁移到 Prisma ORM 时,将 versionKey
字段标记为可选 ( ? ) 在您的 Prisma 模式中,并添加 @ignore
属性以将其从 Prisma Client 中排除。
集合名称推断
Mongoose 会通过自动将模型名称转换为小写和复数形式来推断集合名称。
另一方面,Prisma ORM 会将模型名称映射到数据库中的表名称 建模您的数据。
您可以强制 Mongoose 中的集合名称与模型名称相同,方法是在创建模式时设置 选项
const PostSchema = new Schema(
{
title: String,
content: String,
// more fields here
},
{
collection: 'Post',
}
)
建模关系
您可以使用 Mongoose 在文档之间建模关系,方法是使用 子文档 或存储对其他文档的 引用。
Prisma ORM 允许您在使用 MongoDB 时对文档之间的不同类型的关系进行建模