关于 migrating-motoko-enhanced
面向 Motoko 开发者的增强多迁移技能,用迁移模块链管理 canister 状态演进,并由编译器检查链条与最终状态的一致性。
主要功能
- 01按顺序组合迁移模块
- 02添加、删除和重命名字段
- 03转换字段类型与数据结构
- 04编译器验证迁移链兼容性
- 05升级时仅运行未应用的迁移
适用场景
- 01为持久化 Actor 字段添加或移除属性
- 02在 canister 升级时重构状态
- 03迁移集合中记录的数据结构
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill migrating-motoko-enhanced安装后在 Claude Code 中输入/migrating-motoko-enhanced确认已加载。
详细介绍
增强型多迁移
通过一系列迁移模块管理 canister 状态的演变。每个迁移对应一项逻辑变更(添加、重命名、删除或转换字段),编译器会验证整个迁移链的一致性。
适用场景
- 添加、删除或重命名持久化 actor 字段
- 更改字段类型
- 在 canister 升级期间重组状态
- 项目已在
mops.toml中配置[canisters.<name>.migrations]
重要规则
- 绝不要使用
stable关键字、preupgrade/postupgrade或内联(with migration = ...) - 声明 actor 变量时不带初始化器——值来自迁移链
- actor 主体必须是静态的(除
<system>调用(例如计时器)外,不得有顶层副作用) - 每个迁移文件都导出
public func migration({...}) : {...} - 文件按字典序应用——请使用时间戳前缀
目录结构
backend/
├── main.mo
├── types.mo
├── lib/
├── mixins/
└── migrations/
├── 20250101_000000_Init.mo
├── 20250315_120000_AddProfile.mo
└── 20250601_090000_RenameField.mo
Actor 语法
使用增强型迁移时,actor 变量不带初始化器:
actor {
var name : Text; // value comes from migration chain
var balance : Nat; // likewise
let frozen : Bool; // let bindings can also be uninitialized
public func greet() : async Text {
"Hello, " # name # "! Balance: " # debug_show balance;
};
};
迁移模块结构
每个迁移模块接收一个包含输入字段的 record,并返回一个包含输出字段的 record:
// migrations/20250101_000000_Init.mo
module {
public func migration(_ : {}) : { name : Text; balance : Nat } {
{ name = ""; balance = 0 }
}
}
输入/输出字段语义
| 字段出现于 | 效果 |
|---|---|
| 输入和输出中 | 字段被转换(读取旧值,生成新值) |
| 仅输出中 | 向状态添加新字段 |
| 仅输入中 | 消耗字段并将其从状态中删除 |
| 两者均不出现 | 字段原样保留 |
给定状态 {a : Nat; b : Text; c : Bool} 和迁移:
module {
public func migration(old : { a : Nat; b : Text }) : { a : Int; d : Float } {
{ a = old.a; d = 1.0 }
}
}
a:从Nat转换为Intb:被消耗(删除)c:原样保留d:新增- 结果:
{a : Int; c : Bool; d : Float}
常见模式
初始化状态(第一个迁移,始终必需)
// migrations/20250101_000000_Init.mo
module {
public func migration(_ : {}) : { count : Nat; header : Text } {
{ count = 0; header = "default" }
}
}
添加字段
// migrations/20250201_000000_AddEmail.mo
module {
public func migration(_ : {}) : { email : Text } {
{ email = "" }
}
}
添加可选字段
module {
public func migration(_ : {}) : { assignee : ?Principal } {
{ assignee = null }
}
}
更改字段类型
// migrations/20250301_000000_CountToInt.mo
module {
public func migration(old : { count : Nat }) : { count : Int } {
{ count = old.count }
}
}
重命名字段
// migrations/20250401_000000_RenameHeader.mo
module {
public func migration(old : { header : Text }) : { title : Text } {
{ title = old.header }
}
}
删除字段
// migrations/20250501_000000_DropEmail.mo
module {
public func migration(_ : { email : Text }) : {} {
{}
}
}
转换数据(拆分字段)
// migrations/20250601_000000_SplitName.mo
import Text "mo:core/Text";
module {
public func migration(old : { name : Text }) : { firstName : Text; lastName : Text } {
let parts = old.name.split(#char ' ');
let first = switch (parts.next()) { case (?f) f; case (null) "" };
let last = switch (parts.next()) { case (?l) l; case (null) "" };
{ firstName = first; lastName = last }
}
}
将 Bool 转换为变体
module {
public func migration(old : { var completed : Bool }) : { var status : { #pending; #completed } } {
{ var status = if (old.completed) { #completed } else { #pending } }
}
}
映射集合
import Map "mo:core/Map";
module {
type OldTask = { id : Nat; title : Text; var completed : Bool };
type NewTask = { id : Nat; title : Text; var status : { #pending; #completed } };
public func migration(old : { var tasks : Map.Map<Nat, OldTask> })
: { var tasks : Map.Map<Nat, NewTask> } {
let tasks = old.tasks.map<Nat, OldTask, NewTask>(
func(_, task) {
{
id = task.id;
title = task.title;
var status = if (task.completed) { #completed } else { #pending };
}
}
);
{ var tasks }
}
}
为 Map 中的每条记录添加字段
import Map "mo:core/Map";
module {
type OldUser = { name : Text; email : Text };
type NewUser = { name : Text; email : Text; bio : Text };
public func migration(old : { users : Map.Map<Nat, OldUser> })
: { users : Map.Map<Nat, NewUser> } {
let users = old.users.map<Nat, OldUser, NewUser>(
func(_, u) { { u with bio = "" } }
);
{ users }
}
}
迁移如何组合
迁移构成一个链。编译器会验证每个迁移的输入是否与所有前序迁移生成的状态兼容。
| 迁移 | 输入 | 输出 | 效果 |
|---|---|---|---|
Init | {} | {name : Text; balance : Nat} | 初始化两个字段 |
AddProfile | {} | {profile : Text} | 添加新字段 |
RenameField | {name : Text} | {displayName : Text} | 将 name 重命名为 displayName |
完整迁移链执行后的状态:{displayName : Text; balance : Nat; profile : Text}。actor 必须声明与此最终状态兼容的字段。
生命周期示例:Todo 应用
展示如何在四次部署中组合这些模式。
// migrations/20250101_000000_Init.mo
module {
public func migration(_ : {}) : { var nextId : Nat } {
{ var nextId = 0 }
}
}
// migrations/20250201_000000_AddTasks.mo
import Map "mo:core/Map";
module {
type Task = { id : Nat; text : Text; completed : Bool };
public func migration(_ : {}) : { tasks : Map.Map<Nat, Task> } {
{ tasks = Map.empty<Nat, Task>() }
}
}
// migrations/20250301_000000_TaskStatus.mo — transform Bool → variant
import Map "mo:core/Map";
module {
type OldTask = { id : Nat; text : Text; completed : Bool };
type NewTask = { id : Nat; text : Text; status : { #pending; #inProgress; #completed } };
public func migration(old : { tasks : Map.Map<Nat, OldTask> })
: { tasks : Map.Map<Nat, NewTask> } {
let tasks = old.tasks.map<Nat, OldTask, NewTask>(
func(_, task) {
{ id = task.id; text = task.text;
status = if (task.completed) #completed else #pending }
}
);
{ tasks }
}
}
// migrations/20250401_000000_AddDueDate.mo — add field to each record
import Map "mo:core/Map";
module {
type Status = { #pending; #inProgress; #completed };
type OldTask = { id : Nat; text : Text; status : Status };
type NewTask = { id : Nat; text : Text; status : Status; due : Int };
public func migration(old : { tasks : Map.Map<Nat, OldTask> })
: { tasks : Map.Map<Nat, NewTask> } {
let tasks = old.tasks.map<Nat, OldTask, NewTask>(
func(_, task) { { task with due = 0 } }
);
{ tasks }
}
}
最终状态:{ var nextId : Nat; tasks : Map.Map<Nat, { id : Nat; text : Text; status : { #pending; #inProgress; #completed }; due : Int }> }
运行时行为
- 全新部署时:按顺序运行所有迁移
- 升级时:只运行尚未应用的迁移(跳过已应用的迁移)
- 快进:可以安全地跳过中间部署;所有尚未应用的迁移都会按顺序运行
- 如果迁移触发陷阱,升级会中止,canister 会保留在旧版本
mops.toml 配置
[moc]
args = ["--default-persistent-actors"]
[canisters.backend]
main = "src/backend/main.mo"
[canisters.backend.migrations]
chain = "src/backend/migrations"
配置 [canisters.<name>.migrations] 后,mops 会自动在 check/build/check-stable 中注入 --enhanced-migration。请勿将 --enhanced-migration 添加到 [canisters.<name>].args 中,否则 mops 会报错。
默认启用 --enhanced-orthogonal-persistence。
之后,mops check --fix 和 mops build 可照常运行。将新的迁移文件直接放在 migrations/ 下,并添加时间戳前缀。
限制
- 不能将
--enhanced-migration与内联(with migration = ...)结合使用 - 需要启用增强型正交持久化
- Actor 变量不得带有初始化器
- Actor 主体必须是静态的(除
<system>调用外,不得有顶层副作用) - 每次迁移后的状态必须与下一次迁移的输入兼容
- 最终状态必须与 Actor 声明的字段匹配
- 如果最后一次迁移的输出中包含 Actor 未声明的字段,则会被拒绝
检查清单
-
migrations/目录与 Actor 源文件位于同一目录下 - 第一个迁移初始化所有字段(使用空输入的
Init.mo) - 文件名带有时间戳前缀,以确保顺序正确
- 每个文件都导出
public func migration({...}) : {...} - Actor 变量声明时不带初始化器
- 在
mops.toml中配置[canisters.<name>.migrations](mops 会注入--enhanced-migration) - 运行
mops check --fix验证迁移链的一致性 - 运行
mops build进行编译
其他参考资料
- 加载
motoko,查看 Motoko 语言概述和 mo:core API - 加载
migrating-motoko,了解不使用--enhanced-migration的内联迁移 - 加载
mops-cli,了解mops check、mops build和工具链配置