数据库迁移指南 - 移除 title 字段
📋 变更概述
本次迁移主要包含以下变更:
1. Menu 表变更
- 移除字段:
title- 菜单标题(与menuName重复) - 保留字段:
menuName- 菜单名称(作为唯一的菜单显示名称)
2. 业务逻辑变更
- 新增验证: 目录类型菜单(menuType=1)不能有父菜单(parentId 必须为 null)
- 更新所有引用
title字段的代码为menuName
🚀 执行迁移步骤
方式一:使用 SQL 手动迁移(推荐)
sql
-- 1. 如果需要保留 title 数据,先备份到 menuName(如果 menuName 为空)
UPDATE "menus"
SET "menu_name" = "title"
WHERE "menu_name" = '' OR "menu_name" IS NULL;
-- 2. 删除 title 字段
ALTER TABLE "menus"
DROP COLUMN IF EXISTS "title";方式二:使用 Prisma db push(会丢失数据)
⚠️ 警告: 此方法会丢失所有数据!
bash
cd apps/backend
npx prisma db push --force-reset
pnpm seed # 重新填充测试数据⚠️ 迁移前注意事项
1. 备份数据库
bash
# 备份当前数据库
pg_dump -h your-host -U your-user -d your-db > backup_remove_title.sql2. 数据迁移策略
由于 title 和 menuName 是重复字段,迁移策略:
sql
-- 检查是否有 menuName 为空但 title 有值的记录
SELECT id, "route_name", "menu_name", "title"
FROM "menus"
WHERE ("menu_name" = '' OR "menu_name" IS NULL) AND "title" IS NOT NULL;
-- 如果有,先同步数据
UPDATE "menus"
SET "menu_name" = "title"
WHERE ("menu_name" = '' OR "menu_name" IS NULL) AND "title" IS NOT NULL;3. 验证目录类型菜单
sql
-- 检查是否有 menuType=1 但 parentId 不为 null 的记录
SELECT id, "route_name", "menu_name", "menu_type", "parent_id"
FROM "menus"
WHERE "menu_type" = 1 AND "parent_id" IS NOT NULL;
-- 如果有,需要手动处理:
-- 选项1:将这些菜单改为 menuType=2(普通菜单)
UPDATE "menus"
SET "menu_type" = 2
WHERE "menu_type" = 1 AND "parent_id" IS NOT NULL;
-- 选项2:清除 parentId(将它们变为顶级目录)
UPDATE "menus"
SET "parent_id" = NULL
WHERE "menu_type" = 1 AND "parent_id" IS NOT NULL;✅ 迁移后验证
1. 检查表结构
sql
-- 查看 menus 表结构
\d menus
-- 确认:
-- - 无 title 字段
-- - 有 menu_name 字段2. 验证数据
sql
-- 查看所有菜单的 menuName
SELECT id, "route_name", "menu_name", "menu_type", "parent_id"
FROM "menus"
ORDER BY "order";
-- 确认没有空的 menu_name
SELECT COUNT(*)
FROM "menus"
WHERE "menu_name" = '' OR "menu_name" IS NULL;
-- 确认目录类型菜单都没有父菜单
SELECT COUNT(*)
FROM "menus"
WHERE "menu_type" = 1 AND "parent_id" IS NOT NULL;
-- 结果应该为 03. 测试应用
bash
# 重新生成 Prisma Client
npx prisma generate
# 运行类型检查
npx tsc --noEmit
# 启动应用测试
pnpm dev🔄 回滚方案
如果需要回滚:
sql
-- 1. 重新添加 title 字段
ALTER TABLE "menus"
ADD COLUMN "title" TEXT;
-- 2. 从 menu_name 复制数据到 title
UPDATE "menus"
SET "title" = "menu_name";
-- 3. 设置 title 为 NOT NULL
ALTER TABLE "menus"
ALTER COLUMN "title" SET NOT NULL;📊 代码变更摘要
1. Schema 变更
文件: prisma/schema.prisma
diff
model Menu {
routeName String @unique @map("route_name")
routePath String @map("route_path")
menuName String @default("") @map("menu_name")
- title String
i18nKey String? @map("i18n_key")
// ...
}2. DTO 变更
文件: src/modules/menus/dto/create-menu.dto.ts
diff
export class CreateMenuDto {
@IsString()
menuName: string;
- @IsString()
- title: string;
// ...
}3. Service 变更
文件: src/modules/menus/menus.service.ts
diff
private readonly menuSelect = {
id: true,
routeName: true,
menuName: true,
- title: true,
// ...
};
// 搜索逻辑更新
if (search) {
where.OR = [
- { title: { contains: search } },
+ { menuName: { contains: search } },
{ routeName: { contains: search } },
];
}
// 新增验证逻辑
async create(createMenuDto: CreateMenuDto) {
+ // 验证:目录类型(menuType=1)不能有父菜单
+ if (menuType === 1 && parentId) {
+ throw new BadRequestException('目录类���菜单不能设置父菜单');
+ }
// ...
}4. 其他服务变更
文件:
src/modules/permissions/permissions.service.ts(2处)src/modules/roles/roles.service.ts(1处)
所有引用 menu.title 的地方都改为 menu.menuName
🆘 常见问题
Q1: 为什么要删除 title 字段?
A: title 和 menuName 是重复字段,都用于显示菜单名称。保留一个可以简化数据模型,减少混淆。
Q2: 为什么选择保留 menuName 而不是 title?
A:
menuName更符合语义(菜单名称 vs 标题)- 数据库字段名为
menu_name,更清晰 - 与
routeName命名风格一致
Q3: 目录类型不能有父菜单的原因?
A: 目录(menuType=1)是顶级容器,用于组织菜单层级结构。它本身应该是顶层的,不应该嵌套在其他菜单下。只有普通菜单(menuType=2)才应该有父菜单。
Q4: 现有数据如何处理?
A:
- 如果
menuName为空,从title复制数据 - 如果��� menuType=1 且 parentId 不为空的记录,需要手动决定:
- 改为 menuType=2(普通菜单)
- 或清除 parentId(变为顶级目录)
迁移日期: 2025-10-27 影响范围: Menu 表结构、菜单相关 DTO、Service、Controller 兼容性: 不兼容旧 API(需要客户端同步更新)