name: bknd-modify-schema description: 用于修改现有Bknd模式。涵盖重命名实体、重命名字段、更改字段类型、修改字段约束、处理破坏性更改、数据迁移策略和同步工作流。
修改模式
修改Bknd中的现有模式:重命名实体/字段、更改字段类型或修改约束。
先决条件
- 现有具有实体的Bknd应用(参见
bknd-create-entity) - 对于代码模式:访问
bknd.config.ts - 在破坏性更改之前备份数据库
关键概念:破坏性 vs 非破坏性更改
Bknd的模式同步检测您的代码和数据库之间的差异。一些更改是安全的;其他更改会导致数据丢失。
非破坏性(安全)
- 添加新实体
- 添加新字段(可为空或具有默认值)
- 添加新索引
- 放宽约束(移除
.required())
破坏性(数据丢失风险)
- 重命名实体(视为删除旧 + 创建新)
- 重命名字段(视为删除旧 + 创建新)
- 更改字段类型(可能失败或截断数据)
- 移除字段(删除列和数据)
- 移除实体(删除表和所有数据)
- 对现有数据收紧约束
何时使用 UI vs 代码
使用 UI 模式当
- 交互式探索模式更改
- 快速原型设计(数据丢失可接受)
- 无需版本控制
使用代码模式当
- 生产模式更改
- 需要版本控制
- 团队协作
- 可重复部署
重命名实体
警告: Bknd没有原生重命名。重命名 = 删除旧 + 创建新 = 数据丢失。
安全方法:数据迁移
- 使用所需名称创建新实体
- 将数据从旧迁移到新
- 更新代码引用
- 删除旧实体
代码方法
// 步骤1:在旧实体旁边添加新实体
const schema = em({
// 旧 - 稍后将移除
posts: entity("posts", {
title: text().required(),
content: text(),
}),
// 新 - 所需名称
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});
// 步骤2:迁移数据(通过脚本或CLI运行一次)
const api = app.getApi();
const oldData = await api.data.readMany("posts", { limit: 10000 });
for (const item of oldData.data) {
await api.data.createOne("articles", {
title: item.title,
content: item.content,
});
}
// 步骤3:从模式中移除旧实体
const schema = em({
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});
# 步骤4:强制同步以删除旧表
npx bknd sync --force
UI 方法
- 打开管理面板 (
http://localhost:1337) - 转到 数据 部分
- 使用所需名称创建新实体
- 手动复制字段定义
- 从旧实体导出数据(如果需要)
- 将数据导入到新实体
- 删除旧实体
重命名字段
警告: Bknd将字段重命名视为删除 + 创建 = 该列的数据丢失。
安全方法:数据迁移
// 步骤1:在旧字段旁边添加新字段
const schema = em({
users: entity("users", {
name: text(), // 旧 - 将被移除
full_name: text(), // 新 - 所需名称
}),
});
// 步骤2:迁移数据
const api = app.getApi();
const users = await api.data.readMany("users", { limit: 10000 });
for (const user of users.data) {
if (user.name && !user.full_name) {
await api.data.updateOne("users", user.id, {
full_name: user.name,
});
}
}
// 步骤3:移除旧字段
const schema = em({
users: entity("users", {
full_name: text(),
}),
});
# 步骤4:强制同步以删除旧列
npx bknd sync --force
UI 方法
- 添加具有所需名称的新字段
- 编写脚本或手动复制数据
- 删除旧字段
更改字段类型
类型更改有风险。一些转换有效;其他失败或截断。
兼容的类型更改
| 从 | 到 | 备注 |
|---|---|---|
text |
text (具有不同约束) |
通常安全 |
number |
text |
安全(数字变为字符串) |
boolean |
number |
安全(0/1值) |
boolean |
text |
安全(“true”/“false”) |
不兼容的类型更改
| 从 | 到 | 风险 |
|---|---|---|
text |
number |
如果非数字数据则失败 |
text |
boolean |
如果不是 “true”/“false”/0/1 则失败 |
text |
date |
如果不是有效日期格式则失败 |
json |
text |
可能截断;丢失结构 |
类型更改的安全方法
// 步骤1:添加具有新类型的新字段
const schema = em({
products: entity("products", {
price: text(), // 旧 - 字符串价格
price_cents: number(), // 新 - 整数分
}),
});
// 步骤2:转换并迁移数据
const api = app.getApi();
const products = await api.data.readMany("products", { limit: 10000 });
for (const product of products.data) {
if (product.price && !product.price_cents) {
const cents = Math.round(parseFloat(product.price) * 100);
await api.data.updateOne("products", product.id, {
price_cents: cents,
});
}
}
// 步骤3:移除旧字段,如果需要重命名新字段
const schema = em({
products: entity("products", {
price_cents: number(),
}),
});
更改字段约束
使字段必需
风险: 如果现有记录有空值则失败。
// 之前
entity("users", {
email: text(), // 可选
});
// 之后
entity("users", {
email: text().required(), // 现在必需
});
安全方法:
- 首先更新所有空值
- 然后添加
.required()
// 步骤1:用默认值填充空值
const api = app.getApi();
const usersWithNull = await api.data.readMany("users", {
where: { email: { $isnull: true } },
});
for (const user of usersWithNull.data) {
await api.data.updateOne("users", user.id, {
email: "unknown@example.com",
});
}
// 步骤2:现在安全地添加 .required()
使字段唯一
风险: 如果存在重复则失败。
// 之前
entity("users", {
username: text(),
});
// 之后
entity("users", {
username: text().unique(),
});
安全方法:
- 查找并解决重复项
- 然后添加
.unique()
// 通过原始SQL或手动检查查找重复项
// 通过更新或删除解决重复项
// 然后添加 .unique() 约束
移除必需/唯一
通常安全:
// 之前
entity("users", {
email: text().required().unique(),
});
// 之后 - 放宽约束是安全的
entity("users", {
email: text(), // 现在可选,非唯一
});
同步工作流
预览更改(试运行)
# 查看同步将做什么而不应用
npx bknd sync
输出显示:
- 要创建的新实体/字段
- 要删除的实体/字段
- 索引更改
应用非破坏性更改
# 仅应用添加性更改
npx bknd sync
应用所有更改(包括删除)
# 警告:这将删除表/列
npx bknd sync --force
仅应用删除
# 专门启用删除操作
npx bknd sync --drop
UI 方法:字段修改
更改字段类型
- 在数据部分打开实体
- 点击字段进行编辑
- 注意: 类型下拉菜单可能对现有字段锁定
- 如果锁定:创建具有正确类型的新字段,迁移数据,删除旧字段
更改约束
- 在数据部分打开实体
- 点击字段进行编辑
- 根据需要切换必需/唯一
- 点击 保存
- 点击 同步数据库
重命名字段
- 创建具有所需名称的新字段
- 手动复制数据或编写迁移脚本
- 删除旧字段
- 同步数据库
常见陷阱
同步在类型更改上失败
错误: 无法将列类型从 X 转换为 Y
修复: 使用迁移方法 - 创建新字段,复制数据,删除旧字段。
同步在必需约束上失败
错误: 列包含空值,无法添加 NOT NULL
修复: 首先将所有空值更新为非空,然后重新同步。
同步在唯一约束上失败
错误: 列存在重复值
修复: 在添加唯一约束之前移除重复项。
重命名后数据丢失
问题: 重命名实体/字段并丢失所有数据。
修复: 不幸的是,数据已丢失。从备份恢复。下次使用迁移方法。
强制标志被忽略
问题: --force 似乎不应用更改。
修复: 检查同步输出以查找实际错误。可能是验证问题,而不是权限。
迁移脚本模板
对于复杂迁移,创建独立脚本:
// scripts/migrate-schema.ts
import { App } from "bknd";
async function migrate() {
const app = new App({
connection: { url: process.env.DB_URL! },
});
await app.build();
const api = app.getApi();
console.log("开始迁移...");
// 从旧结构读取所有记录
const records = await api.data.readMany("old_entity", { limit: 100000 });
console.log(`找到 ${records.data.length} 条记录`);
// 转换并插入到新结构
let migrated = 0;
for (const record of records.data) {
await api.data.createOne("new_entity", {
// 根据需要转换字段
new_field: record.old_field,
});
migrated++;
if (migrated % 100 === 0) {
console.log(`已迁移 ${migrated}/${records.data.length}`);
}
}
console.log("迁移完成!");
process.exit(0);
}
migrate().catch(console.error);
运行:
npx bun scripts/migrate-schema.ts
# 或
npx ts-node scripts/migrate-schema.ts
验证
模式修改后
# 1. 检查同步状态
npx bknd sync
# 2. 在调试输出中验证模式
npx bknd schema --pretty
通过代码
const api = app.getApi();
// 通过查询验证字段存在
const result = await api.data.readMany("entity_name", { limit: 1 });
console.log(result.data[0]); // 检查字段名称/值
通过 UI
- 在数据部分打开实体
- 验证字段正确显示
- 使用新模式创建测试记录
- 查询现有记录以验证数据
该做和不该做
该做:
- 在破坏性更改之前备份数据库
- 对于重命名使用迁移方法
- 在强制之前使用
npx bknd sync预览 - 首先在开发数据库上测试
- 保持旧结构直到数据迁移
不该做:
- 直接重命名实体/字段(数据丢失)
- 不预览就使用
--force - 更改类型没有迁移计划
- 向有空数据的字段添加
.required() - 向有重复项的字段添加
.unique()
相关技能
- bknd-create-entity - 创建新实体
- bknd-add-field - 向实体添加字段
- bknd-delete-entity - 安全移除实体
- bknd-seed-data - 填充迁移数据
- bknd-crud-update - 在迁移期间更新记录