Loopback 3更新模型属性时如何迁移旧数据避免数据丢失
LoopBack 3 搭配 MongoDB 存量模型结构变更无丢失迁移方案
核心原则是分阶段兼容+批量迁移,绝对不能直接上线新结构模型,否则存量旧格式数据要么读取报错,要么被默认值覆盖丢失。
1. 先上线过渡兼容版本
不要直接修改模型属性的类型定义,先做一层读写兼容,保证新旧格式数据都能正常访问,新写入的数据自动转为新结构:
- 首先临时放宽变更字段的类型校验,比如原来的
address是string,要改成对象,就先把字段类型设为any,避免LoopBack的类型校验把旧string值过滤掉。
过渡阶段模型属性配置示例:
{ "properties": { "address": { "type": "any", "default": {} } } }
- 在对应模型的JS文件里添加两个钩子,分别处理读取和写入的格式兼容:
// common/models/your-target-model.js module.exports = function(YourModel) { // 读取数据时自动把旧格式转成新结构返回 YourModel.observer('loaded', (ctx, next) => { const data = ctx.instance || ctx.data; if (!data) return next(); // 处理address字段拆分逻辑 if (typeof data.address === 'string') { const oldVal = data.address; // 按你实际的业务拆分规则处理,这里举逗号分隔的例子,拆分失败就把旧值存在street字段,绝对不丢数据 const [street, city, zipcode] = oldVal.split(',').map(i => i?.trim() || ''); data.address = { street: street || oldVal, city: city || '', zipcode: zipcode || '' }; } // 如果是字段重命名场景(比如address改addressline),加这段逻辑 // if (data.address && !data.addressline) { // data.addressline = data.address; // delete data.address; // } next(); }); // 写入数据时统一转成新结构,避免后续继续产生旧格式数据 YourModel.observer('before save', (ctx, next) => { const data = ctx.instance || ctx.data; if (!data) return next(); if (typeof data.address === 'string') { const oldVal = data.address; const [street, city, zipcode] = oldVal.split(',').map(i => i?.trim() || ''); data.address = { street: street || oldVal, city: city || '', zipcode: zipcode || '' }; } next(); }); };
过渡版本上线后,业务侧不会感知到数据格式差异,所有新写入的数据都会自动存为新结构,旧数据读取时也会自动转成新格式返回,不会出现报错或丢值。
2. 批量迁移存量数据
过渡版本上线稳定运行后,跑一次性脚本把所有存量旧格式数据批量更新为新结构,脚本可以放在LoopBack的boot目录下执行,注意加执行标记避免重复跑:
// server/boot/run-migration.js const fs = require('fs'); const path = require('path'); const markFile = path.resolve(__dirname, '.migration_done'); module.exports = async function(app) { // 已经跑过迁移就直接跳过 if (fs.existsSync(markFile)) return; const YourModel = app.models.YourModel; // 只查询格式不符合新结构的旧数据 const filter = { where: { // 匹配address是字符串的旧数据 address: {type: 'string'} } }; // 数据量大的话不要用find全量查,用MongoDB游标分批拉取,避免内存溢出 let count = 0; await YourModel.find(filter).eachAsync(async (doc) => { // 转换逻辑和前面钩子的逻辑保持完全一致 const oldVal = doc.address; const [street, city, zipcode] = oldVal.split(',').map(i => i?.trim() || ''); doc.address = { street: street || oldVal, city: city || '', zipcode: zipcode || '' }; await doc.save(); count++; }, {concurrency: 10}); // 控制并发数,避免打满数据库负载 console.log(`迁移完成,共更新${count}条旧数据`); // 写入标记文件,避免下次启动重复执行 fs.writeFileSync(markFile, ''); };
注意:迁移前一定要先做数据库全量备份,万一转换规则有问题可以随时回滚。
3. 切换到正式模型结构
确认所有存量数据都已经转换完成后,就可以把模型里的字段类型改成正式的新结构,移除临时的any类型配置:
{ "properties": { "address": { "type": { "street": { "type": "string" }, "city": { "type": "string" }, "zipcode": { "type": "string" } } } } }
建议保留之前的loaded兼容钩子1-2个版本,防止有漏迁移的边角数据导致报错,确认没有问题后再移除即可。
如果是字段重命名的场景,逻辑完全一致:过渡阶段做读写映射,批量刷数重命名字段,最后移除旧字段定义即可。
内容的提问来源于stack exchange,提问作者Billy Cottrell
相关产品推荐
相关产品推荐

