之前的一篇文章,介绍了在Nestjs中如何引入Prisma ORM, 如何使用Docker部署。介绍了两种方式Docker部署的方式,即源码构建部署和pkg打包二进制部署。
Nestjs+Prisma ORM+pkg(一)------ Prisma的使用及Docker部署 - 掘金 (juejin.cn)
这篇文章主要讲解一下prisma采用的migration机制,如何来保持schema和数据库一致。以及在生产环境如何执行迁移脚本(migrations)以更好的部署和运维。
以下示例源码:mobiusy/prisma-example (github.com)
Migration
作用
为什么要有migration机制?
我们在日常开发过程中,随着版本的迭代,数据库会发生表的新增,修改等操作,这些操作如果由人工去管理变更,和去数据库执行,就太过于繁琐,且容易发生错误。
Getting started | Prisma Migrate
因此才需要由migration机制来维护这些变更,并且能智能的应用的数据库。这个机制能做到:
- 自动创建数据库
- 数据库增量更新
- 检查数据库定义和实际数据库是否一致
- 等...
演练
- 这次对之前的prisma.schema稍作改动, 新增字段birth, 在这里我们定义为可空字段:
kotlin
model User {
id Int @id @default(autoincrement())
name String
email String
birth String? // 新增
posts Post[]
comments Comment[]
}
- 生成迁移脚本
prisma migrate dev
命令通常在dev, staging环境中执行,它能够跟踪你的对数据库的改动,并自动的生成SQL迁移文件,然后应用到目标数据库。当一个迁移文件(migration)被应用到数据库的时候,_prisma_migrations表会被同时更新。
bash
$ yarn prisma migrate dev
yarn run v1.22.22
Environment variables loaded from .env
Prisma schema loaded from src\db\postgresql\schema.prisma
Datasource "db": PostgreSQL database "prisma-example", schema "public" at "192.168.3.2:5432"
√ Enter a name for the new migration: ... add_birth_to_user
Applying migration `20240422015141_add_birth_to_user`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20240422015141_add_birth_to_user/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (v5.12.1) to .\node_modules@prisma\client in 128ms
生成的迁移脚本在目录src\db\postgresql\migrations\20240422015141_add_birth_to_user
下,内容为:
sql
-- AlterTable
ALTER TABLE "User" ADD COLUMN "birth" TEXT;
- 将脚本应用到生产环境
修改.env文件,将地址指向生产环境数据库
bash
$ yarn prisma migrate deploy
此时数据库的改动已经应用到了生产数据库。
如何在生产环境使用migrate deploy
通过前面的演练我们知道,可以通过yarn prisma migrate deploy
命令来应用迁移脚本(migrations),但这样直接在命令行中执行真的好吗?
考虑以下几个问题:
- 在哪里执行
yarn prisma migrate deploy
命令?在本地直连生产数据库,还是远程到生产环境服务器,并进入到代码环境执行? - 若Docker化部署,是否具备条件执行上述命令?pkg打包成二进制可执行文件,是否能够执行?
- 谁来执行上述命令?开发还是运维?
官方给出的一种解决方案是在CI/CD中执行上述命令。那在不具备CI/CD的环境中怎么办?
我的方案
这里给出大家一种普适应更强的方案,即通过增加resetful接口,调用我们的服务代码,完成上述命令的执行。迁移脚本本身就包含在服务代码中,因此这种方式可以保证迁移脚本和服务的版本一致,并且能跟着服务(docker镜像)移动。
- 增加restful接口
执行命令yarn nest generate resource db
bash
$ yarn nest generate resource db
yarn run v1.22.22
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? No
CREATE src/db/db.controller.ts (199 bytes)
CREATE src/db/db.controller.spec.ts (556 bytes)
CREATE src/db/db.module.ts (236 bytes)
CREATE src/db/db.service.ts (90 bytes)
CREATE src/db/db.service.spec.ts (450 bytes)
UPDATE package.json (2433 bytes)
UPDATE src/app.module.ts (381 bytes)
✔ Packages installed successfully.
Done in 97.22s.
- 在
db/db.service.ts
中增加方法migrateSchema
typescript
import { Injectable, Logger } from '@nestjs/common';
import { promisify } from 'node:util';
import { exec as execCb } from 'node:child_process';
import os from 'os';
import path from 'node:path';
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
rmSync,
} from 'node:fs';
import { SchemaMigrationResult } from './dto/db.dto';
@Injectable()
export class DbService {
private readonly logger = new Logger(DbService.name);
private readonly PG_SCHEMA = 'schema.prisma';
private readonly DB_SOURCE = 'postgresql';
private readonly DB_DEST = 'db-tmp';
constructor() {}
/**
* Run schema migration
* @returns
*/
async migrateScheme(): Promise<SchemaMigrationResult> {
const exec = promisify(execCb);
// 检查`${__dirname}//db`目录是否存在,如果存在,则复制到脚本到`${process.cwd()}/db-tmp`目录下
const souceDir = path.join(__dirname, this.DB_SOURCE);
const distDir = path.join(process.cwd(), this.DB_DEST);
try {
if (existsSync(distDir)) {
rmSync(distDir, { recursive: true });
}
if (existsSync(souceDir)) {
this.copyDir(souceDir, distDir);
} else {
const message = `souceDir ${souceDir} not exists!`;
this.logger.error(message);
throw new Error(message);
}
} catch (error) {
throw new Error(`copyDir error: ${error.message}`);
}
const schemaFile = path.join(distDir, this.PG_SCHEMA);
try {
const { stdout, stderr } = await exec(
`prisma migrate deploy --schema=${schemaFile}`,
{
env: {
...process.env,
},
},
);
this.logger.log({
message: 'db migrate deploly result:',
stdout,
stderr,
});
return {
stdout: stdout.split(os.EOL),
stderr: stderr.split(os.EOL),
};
} finally {
// clean up
if (existsSync(distDir)) {
rmSync(distDir, { recursive: true });
}
}
}
private copyDir(src, dest) {
mkdirSync(dest, { recursive: true });
const entries = readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
this.copyDir(srcPath, destPath);
} else {
copyFileSync(srcPath, destPath);
}
}
}
}
新增dto\db.dto.ts
文件
typescript
export class SchemaMigrationResult {
stdout: string[];
stderr: string[];
}
- 修改
nest-cli.json
文件,将sql、schema类型的文件在构建时放入dist目录
json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"assets": ["**/*.sql", "**/*.prisma"],
"watchAssets": true
}
}
- 重新打包成镜像,并运行
bash
$ docker-compose up -d --build prisma-example
- 调用新增的接口测试
为了展示脚本执行的结果,调用接口前删除了prisma-example数据库。
以上流程走通了非pkg打包的场景,针对pkg打包成二进制文件的场景,还有一些额外的工作.
- 修改pkg.Dockerfile, 这是pkg打包所用的Dockerfile
Dockerfile
FROM node:18.14-slim as builder
LABEL Author="mobiusy"
WORKDIR /build
# ENV PKG_CACHE_PATH /build/.pkg-cache
# RUN mkdir -p ${PKG_CACHE_PATH}/v3.4
# COPY fetched-v18.5.0-linux-x64 ${PKG_CACHE_PATH}/v3.4
RUN yarn config set registry https://registry.npmmirror.com
COPY package.json ./package.json
COPY yarn.lock ./yarn.lock
RUN yarn
COPY ./ ./
RUN yarn prisma generate && yarn build
RUN yarn pkg package.json
FROM node:18.14-slim as prod
LABEL Author="mobiusy"
RUN apt-get update
RUN apt-get install inetutils-ping -y
RUN apt-get install jq -y
# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
WORKDIR /opt/application
COPY --from=builder /build/package.json ./
# 提取nodejs项目中pakcage.json安装的prisma版本号,然后安装到全局
RUN prismaVersion=$(cat package.json | jq '.dependencies["prisma"]' | sed 's/"//g')
RUN npm install -g prisma@$prismaVersion
# 删除package.json文件
RUN rm -rf package.json
# 将生成的可执行文件copy到当前工作目录下
COPY --from=builder /build/nestjs-prisma-example ./
# 容器启动时执行的命令,类似npm run start
CMD ["./nestjs-prisma-example"]
- 重新打包成镜像,并运行
bash
$ docker-compose up -d --build prisma-example-pkg
- 接口测试
为了展示脚本执行的结果,调用接口前删除了prisma-example数据库。注意,这次端口已经变成了33200,通过二进制文件启动的容器服务端口
总结
文章介绍了:
migrate dev
和migrate deploy
命令的使用。- 如何将
migrate deploy
命令集成到容器服务中,并以restful接口的形式暴露出来,方便部署运维。