从 Mongoose 迁移
本指南介绍如何从 Mongoose 迁移到 Prisma ORM。它使用 Mongoose Express 示例 的扩展版本作为 示例项目 来演示迁移步骤。您可以在 GitHub 上找到此指南中使用的示例。
您可以在 Prisma ORM 与 Mongoose 页面上了解 Prisma ORM 与 Mongoose 的比较。
迁移过程概述
请注意,无论您正在构建何种类型的应用程序或 API 层,从 Mongoose 迁移到 Prisma ORM 的步骤始终相同
- 安装 Prisma CLI
- 内省您的数据库
- 安装并生成 Prisma Client
- 逐步用 Prisma Client 替换您的 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 Client API 的基础,它允许您向您的数据库发送查询。
2.4. 更新关系
MongoDB 不支持不同集合之间的关系。但是,您可以使用 ObjectId
字段类型或从一个文档到多个文档使用集合中的 ObjectId
数组来创建文档之间的引用。该引用将存储相关文档的 id。您可以使用 Mongoose 提供的 populate()
方法将引用填充为相关文档的数据。
更新 Post
<-> User
之间的 1-n 关系,如下所示
- 将
posts
模型中现有的author
引用重命名为authorId
并添加@map("author")
属性 - 在
posts
模型中添加author
关系字段及其@relation
属性,指定fields
和references
- 在
users
模型中添加posts
关系
- 差异
- 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
关系 - 在两个模型上都添加一个 关系标量
- 在两侧添加指定
fields
和references
参数的@relation
属性
- 差异
- 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
以指示它是复数形式。
更新 published
字段,包括 @default
属性以定义字段的默认值。
您还可以将 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. 使用 Prisma Client 替换您的 Mongoose 查询
在本节中,我们将展示一些从 Mongoose 迁移到 Prisma Client 的示例查询,这些查询基于示例 REST API 项目中的示例路由。有关 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 时,在您的 Prisma 模式中将 versionKey
字段标记为可选( ? ),并添加 @ignore
属性以将其从 Prisma Client 中排除。
集合名称推断
Mongoose 通过自动将模型名称转换为小写和复数形式来推断集合名称。
另一方面,Prisma ORM 将模型名称映射到数据库中的表名称建模您的数据。
您可以强制 Mongoose 中的集合名称与模型名称相同,方法是在创建模式时设置选项
const PostSchema = new Schema(
{
title: String,
content: String,
// more fields here
},
{
collection: 'Post',
}
)
建模关系
您可以通过使用子文档或存储对其他文档的引用在 Mongoose 中对文档之间的关系进行建模。
使用 MongoDB 时,Prisma ORM 允许您对文档之间的不同类型的关系进行建模