bknd-modify-schema
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseModify Schema
修改Schema
Modify existing schema in Bknd: rename entities/fields, change field types, or alter constraints.
修改Bknd中的现有Schema:重命名实体/字段、变更字段类型或修改约束。
Prerequisites
前提条件
- Existing Bknd app with entities (see )
bknd-create-entity - For code mode: Access to
bknd.config.ts - Backup your database before destructive changes
- 已创建包含实体的Bknd应用(参考)
bknd-create-entity - 代码模式下:需有权限访问
bknd.config.ts - 破坏性变更前请备份数据库
Critical Concept: Destructive vs Non-Destructive Changes
核心概念:破坏性变更 vs 非破坏性变更
Bknd's schema sync detects differences between your code and database. Some changes are safe; others cause data loss.
Bknd的Schema同步功能会检测代码与数据库之间的差异。部分变更安全无风险,部分则会导致数据丢失。
Non-Destructive (Safe)
非破坏性(安全)
- Adding new entities
- Adding new fields (nullable or with default)
- Adding new indices
- Loosening constraints (removing )
.required()
- 添加新实体
- 添加新字段(可空或带有默认值)
- 添加新索引
- 放宽约束(移除)
.required()
Destructive (Data Loss Risk)
破坏性(存在数据丢失风险)
- Renaming entities (treated as drop old + create new)
- Renaming fields (treated as drop old + create new)
- Changing field types (may fail or truncate data)
- Removing fields (drops column and data)
- Removing entities (drops table and all data)
- Tightening constraints on existing data
- 重命名实体(会被识别为删除旧实体+创建新实体)
- 重命名字段(会被识别为删除旧字段+创建新字段)
- 变更字段类型(可能执行失败或截断数据)
- 删除字段(会删除列及对应数据)
- 删除实体(会删除表及所有数据)
- 对现有数据收紧约束
When to Use UI vs Code
UI模式与代码模式的适用场景
Use UI Mode When
适用UI模式的场景
- Exploring schema changes interactively
- Quick prototyping (data loss acceptable)
- No version control needed
- 交互式探索Schema变更
- 快速原型开发(可接受数据丢失)
- 无需版本控制
Use Code Mode When
适用代码模式的场景
- Production schema changes
- Version control required
- Team collaboration
- Reproducible deployments
- 生产环境Schema变更
- 需要版本控制
- 团队协作
- 可重复部署
Renaming an Entity
重命名实体
Warning: Bknd has no native rename. Renaming = DROP old + CREATE new = DATA LOSS.
警告: Bknd不支持原生重命名操作。重命名=删除旧实体+创建新实体=数据丢失。
Safe Approach: Data Migration
安全方案:数据迁移
- Create new entity with desired name
- Migrate data from old to new
- Update code references
- Delete old entity
- 创建带有目标名称的新实体
- 将旧实体的数据迁移至新实体
- 更新代码中的引用
- 删除旧实体
Code Approach
代码实现方案
typescript
// Step 1: Add new entity alongside old
const schema = em({
// OLD - will be removed later
posts: entity("posts", {
title: text().required(),
content: text(),
}),
// NEW - desired name
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});typescript
// Step 2: Migrate data (run once via script or 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,
});
}typescript
// Step 3: Remove old entity from schema
const schema = em({
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});bash
undefinedtypescript
// Step 1: Add new entity alongside old
const schema = em({
// OLD - will be removed later
posts: entity("posts", {
title: text().required(),
content: text(),
}),
// NEW - desired name
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});typescript
// Step 2: Migrate data (run once via script or 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,
});
}typescript
// Step 3: Remove old entity from schema
const schema = em({
articles: entity("articles", {
title: text().required(),
content: text(),
}),
});bash
undefinedStep 4: Sync with force to drop old table
Step 4: Sync with force to drop old table
npx bknd sync --force
undefinednpx bknd sync --force
undefinedUI Approach
UI操作方案
- Open admin panel ()
http://localhost:1337 - Go to Data section
- Create new entity with desired name
- Copy field definitions manually
- Export data from old entity (if needed)
- Import data to new entity
- Delete old entity
- 打开管理面板()
http://localhost:1337 - 进入数据板块
- 创建带有目标名称的新实体
- 手动复制字段定义
- 导出旧实体的数据(如有需要)
- 将数据导入新实体
- 删除旧实体
Renaming a Field
重命名字段
Warning: Bknd treats field renames as drop + create = DATA LOSS on that column.
警告: Bknd会将字段重命名识别为删除旧字段+创建新字段=对应列的数据丢失。
Safe Approach: Data Migration
安全方案:数据迁移
typescript
// Step 1: Add new field alongside old
const schema = em({
users: entity("users", {
name: text(), // OLD - will be removed
full_name: text(), // NEW - desired name
}),
});typescript
// Step 2: Migrate data
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,
});
}
}typescript
// Step 3: Remove old field
const schema = em({
users: entity("users", {
full_name: text(),
}),
});bash
undefinedtypescript
// Step 1: Add new field alongside old
const schema = em({
users: entity("users", {
name: text(), // OLD - will be removed
full_name: text(), // NEW - desired name
}),
});typescript
// Step 2: Migrate data
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,
});
}
}typescript
// Step 3: Remove old field
const schema = em({
users: entity("users", {
full_name: text(),
}),
});bash
undefinedStep 4: Sync with force to drop old column
Step 4: Sync with force to drop old column
npx bknd sync --force
undefinednpx bknd sync --force
undefinedUI Approach
UI操作方案
- Add new field with desired name
- Write script or manually copy data
- Delete old field
- 添加带有目标名称的新字段
- 编写脚本或手动复制数据
- 删除旧字段
Changing Field Type
变更字段类型
Type changes are risky. Some conversions work; others fail or truncate.
类型变更存在风险。部分转换可正常执行,部分会失败或截断数据。
Compatible Type Changes
兼容的类型转换
| From | To | Notes |
|---|---|---|
| | Usually safe |
| | Safe (numbers become strings) |
| | Safe (0/1 values) |
| | Safe ("true"/"false") |
| 原类型 | 目标类型 | 说明 |
|---|---|---|
| | 通常安全 |
| | 安全(数字转为字符串) |
| | 安全(转换为0/1值) |
| | 安全(转换为"true"/"false") |
Incompatible Type Changes
不兼容的类型转换
| From | To | Risk |
|---|---|---|
| | Fails if non-numeric data |
| | Fails if not "true"/"false"/0/1 |
| | Fails if not valid date format |
| | May truncate; loses structure |
| 原类型 | 目标类型 | 风险 |
|---|---|---|
| | 若包含非数字数据则执行失败 |
| | 若值不为"true"/"false"/0/1则执行失败 |
| | 若格式不是有效日期则执行失败 |
| | 可能截断数据并丢失结构 |
Safe Approach for Type Change
类型变更的安全方案
typescript
// Step 1: Add new field with new type
const schema = em({
products: entity("products", {
price: text(), // OLD - string prices
price_cents: number(), // NEW - integer cents
}),
});typescript
// Step 2: Transform and migrate data
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,
});
}
}typescript
// Step 3: Remove old field, rename new if desired
const schema = em({
products: entity("products", {
price_cents: number(),
}),
});typescript
// Step 1: Add new field with new type
const schema = em({
products: entity("products", {
price: text(), // OLD - string prices
price_cents: number(), // NEW - integer cents
}),
});typescript
// Step 2: Transform and migrate data
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,
});
}
}typescript
// Step 3: Remove old field, rename new if desired
const schema = em({
products: entity("products", {
price_cents: number(),
}),
});Changing Field Constraints
修改字段约束
Making a Field Required
设置字段为必填
Risk: Fails if existing records have null values.
typescript
// Before
entity("users", {
email: text(), // Optional
});
// After
entity("users", {
email: text().required(), // Now required
});Safe approach:
- Update all null values first
- Then add
.required()
typescript
// Step 1: Fill nulls with default
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",
});
}
// Step 2: Now safely add .required()风险: 若现有记录存在空值则执行失败。
typescript
// Before
entity("users", {
email: text(), // Optional
});
// After
entity("users", {
email: text().required(), // Now required
});安全方案:
- 先更新所有空值
- 再添加
.required()
typescript
// Step 1: Fill nulls with default
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",
});
}
// Step 2: Now safely add .required()Making a Field Unique
设置字段为唯一
Risk: Fails if duplicates exist.
typescript
// Before
entity("users", {
username: text(),
});
// After
entity("users", {
username: text().unique(),
});Safe approach:
- Find and resolve duplicates
- Then add
.unique()
typescript
// Check for duplicates via raw SQL or manual inspection
// Resolve duplicates by updating or deleting
// Then add .unique() constraint风险: 若存在重复值则执行失败。
typescript
// Before
entity("users", {
username: text(),
});
// After
entity("users", {
username: text().unique(),
});安全方案:
- 查找并解决重复值问题
- 再添加
.unique()
typescript
// Check for duplicates via raw SQL or manual inspection
// Resolve duplicates by updating or deleting
// Then add .unique() constraintRemoving Required/Unique
移除必填/唯一约束
Generally safe:
typescript
// Before
entity("users", {
email: text().required().unique(),
});
// After - loosening constraints is safe
entity("users", {
email: text(), // Now optional, non-unique
});通常是安全的:
typescript
// Before
entity("users", {
email: text().required().unique(),
});
// After - loosening constraints is safe
entity("users", {
email: text(), // Now optional, non-unique
});The Sync Workflow
同步工作流
Preview Changes (Dry Run)
预览变更(试运行)
bash
undefinedbash
undefinedSee what sync would do without applying
See what sync would do without applying
npx bknd sync
Output shows:
- New entities/fields to create
- Entities/fields to drop
- Index changesnpx bknd sync
输出内容包括:
- 待创建的新实体/字段
- 待删除的实体/字段
- 索引变更Apply Non-Destructive Changes
应用非破坏性变更
bash
undefinedbash
undefinedApplies only additive changes
Applies only additive changes
npx bknd sync
undefinednpx bknd sync
undefinedApply All Changes (Including Drops)
应用所有变更(包括删除操作)
bash
undefinedbash
undefinedWARNING: This will drop tables/columns
WARNING: This will drop tables/columns
npx bknd sync --force
undefinednpx bknd sync --force
undefinedApply Drops Only
仅应用删除操作
bash
undefinedbash
undefinedSpecifically enables drop operations
Specifically enables drop operations
npx bknd sync --drop
---npx bknd sync --drop
---UI Approach: Field Modifications
UI操作方案:字段修改
Change Field Type
变更字段类型
- Open entity in Data section
- Click on field to edit
- Note: Type dropdown may be locked for existing fields
- If locked: Create new field with correct type, migrate data, delete old
- 在数据板块中打开对应实体
- 点击字段进行编辑
- 注意: 现有字段的类型下拉菜单可能被锁定
- 若被锁定:创建带有正确类型的新字段,迁移数据后删除旧字段
Change Constraints
修改约束
- Open entity in Data section
- Click on field to edit
- Toggle Required/Unique as needed
- Click Save
- Click Sync Database
- 在数据板块中打开对应实体
- 点击字段进行编辑
- 根据需要切换必填/唯一选项
- 点击保存
- 点击同步数据库
Rename Field
重命名字段
- Create new field with desired name
- Manually copy data or write migration script
- Delete old field
- Sync database
- 创建带有目标名称的新字段
- 手动复制数据或编写迁移脚本
- 删除旧字段
- 同步数据库
Common Pitfalls
常见问题
Sync Fails on Type Change
类型变更时同步失败
Error:
Cannot convert column type from X to YFix: Use migration approach - create new field, copy data, drop old.
错误:
Cannot convert column type from X to Y解决方法: 使用迁移方案 - 创建新字段,复制数据,删除旧字段。
Sync Fails on Required Constraint
设置必填约束时同步失败
Error:
Column contains null values, cannot add NOT NULLFix: Update all null values to non-null first, then re-sync.
错误:
Column contains null values, cannot add NOT NULL解决方法: 先将所有空值更新为非空值,再重新同步。
Sync Fails on Unique Constraint
设置唯一约束时同步失败
Error:
Duplicate values exist for columnFix: Remove duplicates before adding unique constraint.
错误:
Duplicate values exist for column解决方法: 添加唯一约束前先移除重复值。
Data Lost After Rename
重命名后数据丢失
Problem: Renamed entity/field and lost all data.
Fix: Unfortunately, data is gone. Restore from backup. Use migration approach next time.
问题: 重命名实体/字段后所有数据丢失。
解决方法: 数据已无法恢复,请从备份中恢复。下次请使用迁移方案。
Force Flag Ignored
--force参数未生效
Problem: doesn't seem to apply changes.
--forceFix: Check sync output for actual errors. May be validation issue, not permission.
问题: 参数似乎未应用变更。
--force解决方法: 检查同步输出中的实际错误信息。可能是验证问题,而非权限问题。
Migration Script Template
迁移脚本模板
For complex migrations, create a standalone script:
typescript
// 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("Starting migration...");
// Read all records from old structure
const records = await api.data.readMany("old_entity", { limit: 100000 });
console.log(`Found ${records.data.length} records`);
// Transform and insert into new structure
let migrated = 0;
for (const record of records.data) {
await api.data.createOne("new_entity", {
// Transform fields as needed
new_field: record.old_field,
});
migrated++;
if (migrated % 100 === 0) {
console.log(`Migrated ${migrated}/${records.data.length}`);
}
}
console.log("Migration complete!");
process.exit(0);
}
migrate().catch(console.error);Run with:
bash
npx bun scripts/migrate-schema.ts对于复杂迁移,可创建独立脚本:
typescript
// 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("Starting migration...");
// Read all records from old structure
const records = await api.data.readMany("old_entity", { limit: 100000 });
console.log(`Found ${records.data.length} records`);
// Transform and insert into new structure
let migrated = 0;
for (const record of records.data) {
await api.data.createOne("new_entity", {
// Transform fields as needed
new_field: record.old_field,
});
migrated++;
if (migrated % 100 === 0) {
console.log(`Migrated ${migrated}/${records.data.length}`);
}
}
console.log("Migration complete!");
process.exit(0);
}
migrate().catch(console.error);运行脚本:
bash
npx bun scripts/migrate-schema.tsor
or
npx ts-node scripts/migrate-schema.ts
---npx ts-node scripts/migrate-schema.ts
---Verification
验证
After Schema Modification
Schema修改后验证
bash
undefinedbash
undefined1. Check sync status
1. Check sync status
npx bknd sync
npx bknd sync
2. Verify schema in debug output
2. Verify schema in debug output
npx bknd schema --pretty
undefinednpx bknd schema --pretty
undefinedVia Code
通过代码验证
typescript
const api = app.getApi();
// Verify field exists by querying
const result = await api.data.readMany("entity_name", { limit: 1 });
console.log(result.data[0]); // Check field names/valuestypescript
const api = app.getApi();
// Verify field exists by querying
const result = await api.data.readMany("entity_name", { limit: 1 });
console.log(result.data[0]); // Check field names/valuesVia UI
通过UI验证
- Open entity in Data section
- Verify fields appear correctly
- Create test record with new schema
- Query existing records to verify data
- 在数据板块中打开实体
- 确认字段显示正确
- 使用新Schema创建测试记录
- 查询现有记录以验证数据
DOs and DON'Ts
注意事项
DO:
- Back up database before destructive changes
- Use migration approach for renames
- Preview with before forcing
npx bknd sync - Test on development database first
- Keep old structure until data migrated
DON'T:
- Rename entities/fields directly (data loss)
- Use without previewing first
--force - Change types without migration plan
- Add to fields with null data
.required() - Add to fields with duplicates
.unique()
建议:
- 破坏性变更前备份数据库
- 重命名操作使用迁移方案
- 强制执行前先用预览
npx bknd sync - 先在开发数据库中测试
- 数据迁移完成前保留旧结构
禁止:
- 直接重命名实体/字段(会导致数据丢失)
- 未预览就使用参数
--force - 无迁移计划就变更类型
- 为空值字段添加
.required() - 为存在重复值的字段添加
.unique()
Related Skills
相关技能
- bknd-create-entity - Create new entities
- bknd-add-field - Add fields to entities
- bknd-delete-entity - Safely remove entities
- bknd-seed-data - Populate migrated data
- bknd-crud-update - Update records during migration
- bknd-create-entity - 创建新实体
- bknd-add-field - 为实体添加字段
- bknd-delete-entity - 安全删除实体
- bknd-seed-data - 填充迁移后的数据
- bknd-crud-update - 迁移期间更新记录