如何让NestJS Prisma与Next.js Drizzle安全共享PostgreSQL数据库
问题背景
现有一个基于NestJS + Prisma ORM的服务已稳定运行,通过定时任务和外部API操作PostgreSQL数据库中的Plant、Device、DeviceInfo三张表。现在需要让一个使用Drizzle ORM的Next.js项目直接访问同一数据库,但执行Drizzle的迁移命令后,数据库表结构被意外修改,需找到安全共享数据库的方案,避免生产环境出现问题。
现有环境配置
NestJS(Prisma侧)命令
{ "prisma:init": "npx prisma migrate dev --name init", // 仅初始执行一次 "prisma:generate": "npx prisma generate", "prisma:migrate": "npx prisma migrate deploy", "prisma": "npm run prisma:generate && npm run prisma:migrate" // 启动服务前必执行 }
Prisma模型定义
model Plant { id Int @id @default(autoincrement()) vendor String uuid String data Json vendorUuid String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([vendorUuid]) } model Device { id Int @id @default(autoincrement()) vendor String uuid String data Json vendorUuid String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([vendorUuid]) } model DeviceInfo { id Int @id @default(autoincrement()) vendor String uuid String data Json vendorUuid String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([vendorUuid]) }
Next.js(Drizzle侧)命令
npx drizzle-kit generate npx drizzle-kit push
Drizzle Schema定义
// 对齐Prisma的时间字段行为 const timestampColumns = { createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').$onUpdate(() => new Date()).notNull() }; // Plants table export const plantTable = pgTable( 'plants', { id: serial('id').primaryKey(), vendor: varchar('vendor', { length: 255 }).notNull(), uuid: varchar('uuid', { length: 255 }).notNull(), data: jsonb('data').notNull(), vendorUuid: varchar('vendor_uuid', { length: 255 }).notNull().unique(), ...timestampColumns, }, (t) => [index('plant_vendor_uuid_idx').on(t.vendorUuid)] ); export type Plant = typeof plantTable.$inferSelect; export type PlantCreateParams = typeof plantTable.$inferInsert; // Devices table export const deviceTable = pgTable( 'devices', { id: serial('id').primaryKey(), vendor: varchar('vendor', { length: 255 }).notNull(), uuid: varchar('uuid', { length: 255 }).notNull(), data: jsonb('data').notNull(), vendorUuid: varchar('vendor_uuid', { length: 255 }).notNull().unique(), ...timestampColumns, }, (t) => [index('device_vendor_uuid_idx').on(t.vendorUuid)] ); export type Device = typeof deviceTable.$inferSelect; export type DeviceCreateParams = typeof deviceTable.$inferInsert; // Device Info table export const deviceInfoTable = pgTable( 'device_info', { id: serial('id').primaryKey(), vendor: varchar('vendor', { length: 255 }).notNull(), uuid: varchar('uuid', { length: 255 }).notNull(), data: jsonb('data').notNull(), vendorUuid: varchar('vendor_uuid', { length: 255 }).notNull().unique(), ...timestampColumns, }, (t) => [index('device_info_vendor_uuid_idx').on(t.vendorUuid)] ); export type DeviceInfo = typeof deviceInfoTable.$inferSelect; export type DeviceInfoCreateParams = typeof deviceInfoTable.$inferInsert;
Drizzle生成的迁移脚本
CREATE TABLE "device_info" ( "id" serial PRIMARY KEY NOT NULL, "vendor" varchar(255) NOT NULL, "uuid" varchar(255) NOT NULL, "data" jsonb NOT NULL, "vendor_uuid" varchar(255) NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp, CONSTRAINT "device_info_vendor_uuid_unique" UNIQUE("vendor_uuid") ); --> statement-breakpoint CREATE TABLE "devices" ( "id" serial PRIMARY KEY NOT NULL, "vendor" varchar(255) NOT NULL, "uuid" varchar(255) NOT NULL, "data" jsonb NOT NULL, "vendor_uuid" varchar(255) NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp, CONSTRAINT "devices_vendor_uuid_unique" UNIQUE("vendor_uuid") ); --> statement-breakpoint CREATE TABLE "plants" ( "id" serial PRIMARY KEY NOT NULL, "vendor" varchar(255) NOT NULL, "uuid" varchar(255) NOT NULL, "data" jsonb NOT NULL, "vendor_uuid" varchar(255) NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp, CONSTRAINT "plants_vendor_uuid_unique" UNIQUE("vendor_uuid") ); --> statement-breakpoint CREATE INDEX "device_info_vendor_uuid_idx" ON "device_info" USING btree ("vendor_uuid");--> statement-breakpoint CREATE INDEX "device_vendor_uuid_idx" ON "devices" USING btree ("vendor_uuid");--> statement-breakpoint CREATE INDEX "plant_vendor_uuid_idx" ON "plants" USING btree ("vendor_uuid");
安全共享数据库的解决方案
1. 统一迁移源,禁用Drizzle的结构变更能力
因NestJS服务已稳定运行,将Prisma作为唯一的数据库迁移工具,所有表结构变更必须通过Prisma的migrate流程执行:
- 删除Next.js项目中
drizzle-kit push和drizzle-kit generate(生成迁移脚本)的命令,仅保留drizzle-kit generate:types(仅生成TypeScript类型定义)的命令,避免误操作修改数据库结构。 - 表结构变更流程:修改NestJS项目的Prisma模型 → 生成Prisma迁移脚本 → 测试环境验证 → 生产环境执行
prisma:migrate。
2. 严格对齐两个ORM的Schema定义
确保Prisma和Drizzle的Schema完全一致:
- 表名/字段名:保持Prisma默认的驼峰转蛇形映射(如
vendorUuid→vendor_uuid)与Drizzle配置一致。 - 字段类型:Prisma的
Json对应Drizzle的jsonb;Prisma的@updatedAt需对应Drizzle的$onUpdate(() => new Date()),确保时间字段自动更新行为一致。 - 约束与索引:唯一约束、索引的名称和关联字段必须完全匹配,避免重复创建或遗漏。
3. 数据库权限隔离
为Next.js项目创建专用数据库用户,仅授予必要权限:
- 若仅需读取数据,授予
SELECT权限;若需写入,追加INSERT/UPDATE/DELETE权限,禁止授予CREATE/ALTER/DROP等结构修改权限。 - 示例PostgreSQL权限配置:
CREATE USER nextjs_user WITH PASSWORD 'your_secure_password'; -- 授予读权限 GRANT SELECT ON plants, devices, device_info TO nextjs_user; -- 若需写入,追加以下权限 GRANT INSERT, UPDATE, DELETE ON plants, devices, device_info TO nextjs_user; -- 授予自增序列权限 GRANT USAGE, SELECT ON SEQUENCE plants_id_seq, devices_id_seq, device_info_id_seq TO nextjs_user;
4. 规范迁移与发布流程
- 所有表结构变更必须先在测试环境验证:同时启动NestJS和Next.js服务,测试数据读写、定时任务、API调用是否正常,确保变更不影响现有功能。
- 生产环境迁移需在低峰期执行,执行前备份数据库,执行后验证两个项目功能正常。
- 发布Next.js项目前,确认其Drizzle Schema与当前数据库结构完全一致,避免查询错误。
5. 并发与事务控制
- 两个项目都会修改的数据操作必须用事务包裹,避免并发冲突。比如NestJS用Prisma事务,Next.js用Drizzle事务。
- 避免长时间运行的事务,减少锁表时间,降低冲突概率。
6. 监控与告警
- 监控数据库结构变更日志,发现非Prisma迁移的结构变更立即告警。
- 监控两个项目的数据库操作错误日志,及时排查Schema不一致或权限问题。
内容的提问来源于stack exchange,提问作者Maifee Ul Asad
相关产品推荐
相关产品推荐

