migrating-motoko-actors について
直交永続化を使う Motoko canister のアップグレード向けガイダンスです。安定状態の変更で明示的なマイグレーションが必要な条件と、mops 管理のチェーンの作成方法を説明します。
主な機能
- 01暗黙的・明示的な移行の区別
- 02タイムスタンプ付きファイルの規則
- 03安定状態の初期化方法
- 04状態変換パターンの提示
活用シーン
- 01アップグレード時の移行要否の判断
- 02安定 Actor 状態の追加や再構成
- 03canister アップグレード用移行の作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfinity/ icskills --skill migrating-motoko-actorsインストール後、Claude Code で/migrating-motoko-actorsを入力して読み込まれたか確認します。
同梱ファイルreferences/examples.md
詳細
Motoko アクターのマイグレーション
直交永続化を使用して、キャニスターのアップグレードをまたいでアクターの状態を移行するための専門的なガイダンスです。
重要:従来型の永続化やアップグレード前後の処理は絶対に使用しないでください
従来型(レガシー)の永続化や、system func preupgrade / system func postupgrade は使用しないでください。エラーが発生しやすく、トラップするとキャニスターが停止したままになる可能性があり、規模の拡大にも対応できません。
また、レガシーの (with migration = ...) アクター付属構文も使用しないでください。このスキルでは mops で管理するマイグレーションを扱います。明示的なマイグレーションが必要な変更は、src/backend/migrations/ にタイムスタンプ付きファイルとして記述してください。チェーンは自動的にリプレイされます。
安定状態の初期化方法
安定アクターフィールドには、アクター本体内で初期化子を指定しません。値を設定するのは src/backend/migrations/ 内のチェーンです。ランタイムはファイルを辞書順に処理し、新規インストールではすべてのファイルを実行し、アップグレードではキャニスターにまだ適用されていないファイルだけを実行します。
このスキルを使用する場面
- アクターの状態を変更するキャニスターのアップグレードを計画または実装するとき
- 暗黙的なマイグレーションと明示的なマイグレーションのどちらを選ぶか判断するとき
- マイグレーション関数を作成またはリファクタリングするとき
- 安定状態を初めて導入するとき(
OldActor = {}を指定するマイグレーションを作成するか、それがまだ保留中であれば最初のマイグレーションにまとめます)
マイグレーションフォルダーのルール
- すべてのマイグレーションは
src/backend/migrations/に配置します。 新規インストールではチェーン全体をファイル名の辞書順にリプレイし、アップグレードではデプロイ済みの末尾より新しい項目だけを実行します。(ディレクトリはmops.tomlの[canisters.<name>.migrations]にあるchain = ...で宣言されています。デフォルト以外のキャニスター名を使うインポート済みプロジェクトでは、別の場所に配置されている場合があります。決めつけずにmops.tomlを確認してください。) - ビルドごとに保留中のマイグレーションは最大 1 つです(
mops.tomlのcheck-limit = 1)。マイグレーションがすでに保留中(未適用)の場合は、別のファイルを追加せず、そのファイルを編集して以降の変更をまとめてください。mops checkはデプロイ済みの.mostベースラインと比較し、上限を超えたときに変更をまとめる最新の保留中ファイルの名前を表示します。ホスティングプラットフォームがマイグレーションセクションとcheck-limitを管理している場合は、エラーを解消するためにこれらを編集しないでください。 - 新しいファイルには UTC タイムスタンプだけを名前として付け、接尾辞は付けません:
YYYYMMDD_HHMMSS.mo。タイムスタンプは、既存のすべてのファイルより後に並ぶ必要があります。変更内容をファイル名に含めないでください(AddPriority、AddTags、Initなど)。機能を表す名前にすると、次の変更で保留中のファイルを編集する代わりに、別のファイルを追加したくなります。 - 適用済みのマイグレーションは、変更、削除、名前変更を絶対に行わないでください。 適用済みのマイグレーションはモジュール名で追跡されるため、名前を変更するとランタイムはそのファイルを未適用として扱い、適用済みファイルを編集しても実行されません。一部のプラットフォームでは、適用済みのマイグレーションを読み取り専用にしてこれを防ぎます。その場合、それらへの書き込みは失敗します。まだ適用されていないマイグレーションは保留中です。同じ変更に対して 2 つ目のマイグレーションを追加するのではなく、そのファイルを編集してください。
- マイグレーションは自己完結していなければなりません。 古い型と新しい型の両方をマイグレーションファイル内に記述してください。インポートできるのは
mo:core/...と mops パッケージだけです。../typesやプロジェクトのモジュールは絶対にインポートしないでください。チェーンは今後もリプレイされ続けます。Types.Noteをインポートしたマイグレーションは、Noteが互換性のない形で変更された時点で正しくなくなります。パッケージのインポートが許可されるのは、コンポーネント所有の不透明な状態があるためです。たとえば、AccessControl.initState()はmo:caffeineai-authorization/access-controlパッケージをインポートしなければ構築できません。 mops check --fixはアップグレード互換性を自動的に検証します。
2 種類のマイグレーション
1. 暗黙的な移行(コード不要)
ランタイムは、新しいプログラムが古いプログラムと互換性がある場合、アップグレードを許可します。移行関数は不要です。
以下の暗黙的な変更のために、移行ファイルを追加しないでください。 移行ファイルは新規インストールのたびに永久に再実行されます。何もしないファイルを追加すると、チェーンが肥大化し、新規インストールが遅くなり、削除できない固定化された成果物が生まれます。チェーンが長くなりすぎると、リトライしても回復できない形でデプロイ不能になり、
mops checkでも警告されません。移行は少ないほど常に優れています。本体が同一なら移行不要です。
(old : OldActor) : NewActorの本体が単にoldを返すだけ(または変換せずに同じレコードをフィールドごとに再構築するだけ)なら、その変更は安定互換です。ファイルを削除してください。
明示的な移行なしで許可される変更(一般的な互換性のある変更):
- フィールドの可変性を変更する(
varからlet、またはその逆) - バリアントコンストラクターを追加する
NatからInt(およびその他の安全な上位型)に変更する- Motoko の安定サブタイプ規則を満たすその他の変更
明示的な移行が必要な変更:
- 以前のバージョンで公開されていたアクターフィールドを削除する —
OldActorで受け取り、NewActorから省略します Anyやその他の情報を失う上位型へ昇格する- フィールド名を変更する(移行で旧名から新名に対応付けます)
- 単純でない変換(例:
IntからFloat、レコードの構造変更) - 新しい安定フィールドを追加する(初期値は移行から渡す必要があり、インライン初期化子は使用できません)
- 初めて安定状態を導入する(初期値は移行から渡します。アクターでは安定フィールドを型だけで宣言し、インライン初期化子は使用しません)
2. 明示的な移行(移行関数)
新しい状態の形状や型が、古い状態に単純に互換性のある拡張を加えたものではない場合に使用します。
src/backend/migrations/ に新しいタイムスタンプ付きファイルを追加します。チェーンは自動的に再実行されます。
ルール:
- 移行関数の型:
(old : OldActor) : NewActor。両方のレコード型は安定型でなければなりません(共有関数やローカル関数は不可。プリミティブ型、レコード、バリアント、Option、Map、List などを使用します)。 - ドメイン
OldActor:古い安定フィールドのレコード(名前と型は以前のバージョンと同じ)。移行前のアクターに、ドメインが想定するフィールドがない場合、アップグレードはトラップして中止されます。 - 余域
NewActor:新しい安定フィールドのレコード。各フィールドは新しいアクターに同じ名前で存在し、余域の型の上位型でなければなりません。アクターのvarとletに合わせて、出力ではvar x = ...またはx = ...を使用します。 - 新規インストールでは、空のアクター(最初の移行では
OldActor = {})から始めて、チェーン全体を順番に再実行します。アップグレードでは、デプロイ済みの末尾より新しいエントリーのみが実行されます。例外:レガシー永続化から変換されたプロジェクトでは、最初のファイルのOldActorは{}ではなく、変換前の安定状態の形状です。そのままにしてください(troubleshooting-motoko-migrationsを参照)。 - 各
NewActorフィールドの値は、移行本体から取得します。拡張モードでは、アクター本体に初期化子はありません。 - 明確さのため、
OldActorとNewActorの両方にすべての安定フィールド(変更されていないものも含む)を列挙する形式が標準です。変更するフィールドだけを列挙する部分的なOldActor/NewActor(サブセット形式)もサポートされています。変更されていないフィールドは自動的に引き継がれます。いずれの場合も、OldActorにあってNewActorにないフィールドは明示的な破棄(データ損失の可能性あり)です。 - 移行関数がトラップすると、アップグレードは中止され、カニスターは古いバージョンのままです。移行は純粋に保ち、予期せずトラップする可能性のある処理を含めないでください。
複数段階のアップグレード(例:v1からv2、v2からv3):各アップグレード段階には、以前にデプロイされたバージョンからの移行が1つあります。次のバージョンでは新しい移行を使用できます(変更が安定互換であれば、移行なしでもかまいません)。
マイグレーションの作成
既存のすべてのエントリーより後にソートされる、UTC タイムスタンプのみのファイル名(YYYYMMDD_HHMMSS.mo、接尾辞なし)を選び、モジュールを作成します。
module {
type OldActor = { /* old stable fields, inlined */ };
type NewActor = { /* new stable fields */ };
public func migration(old : OldActor) : NewActor {
{ /* produce new fields from old */ };
};
};
public func migration(old : OldActor) : NewActorをエクスポートするmoduleにする必要があります(関数名は必須です。チェーンランナーはこの名前で関数を検出します)。OldActorとNewActorはどちらもファイル内にインラインで記述する必要があります。プロジェクトのインポートはできません。
アクター本体:型のみ、初期化子なし
安定変数は初期値なしで型を宣言します。一時的な let/var フィールドは通常どおり初期化子を使用します。
この初期化子なしのルールは、強化マイグレーションプロジェクト(このスキルの対象)に適用されます。この場合、初期値はマイグレーションチェーンが管理します。強化マイグレーションチェーンのない通常のプロジェクトでは、安定フィールドをアクター本体内で通常どおりインライン値で初期化します(例:
persistent actor { let m = Map.empty<Nat, Text>(); })。
actor {
let tasks : Map.Map<Text, Task>;
var nextId : Nat;
transient var cache = List.empty<Text>();
};
初期値はマイグレーションチェーンから設定されます。初めて安定状態を導入するときは、OldActor = {} とし、main.mo で宣言されているすべての安定フィールドを列挙した NewActor を持つマイグレーションを作成します。マイグレーション本体では、各フィールドの値を生成する必要があります。フィールドが欠けていると互換性警告が発生し、その後のアップグレードが失敗します。
例の中に、
let accessControlState = AccessControl.initState();やlet users = Map.empty<Principal, User>();のようなインライン初期化子がアクター本体に直接記述されている場合、それは強化マイグレーション導入前の例です。強化マイグレーションではコンパイルエラー(M0014、M0250)になります。そのような例は状態の形状を把握するためだけに使用してください。フィールド名と型をアクターにコピーし(初期化子は付けず)、初期化式はマイグレーション関数のNewActor出力に移してください。
安定互換性(定義)
旧シグネチャが新シグネチャと安定互換であるとは、旧シグネチャの安定フィールド id : T ごとに、次を満たすことです。
- 新シグネチャに、安定フィールド
id : Uが存在し、T が U の安定サブタイプであること(旧値をデータ損失なく新しいフィールドの初期値として使用できます)。 - 新シグネチャには、旧シグネチャに存在しないフィールドを追加できます(それぞれマイグレーションで値を指定します)。
- 対応するフィールドは、可変性(
varと非var)が異なっていてもかまいません。
つまり、型を拡張(例:Nat から Int)したり、可変性を変更したりする場合、明示的なマイグレーションは不要です。フィールドの削除、名前の変更、型の縮小はできません。安定フィールドの追加には必ずマイグレーションが必要です。インライン初期化子は使用できないため、新しいフィールドの初期値はマイグレーションファイルの NewActor から設定する必要があります。
パターン
デフォルト値を使ったレコードフィールドの追加
record with newField = value を使って、すべての要素にフィールドを追加します。
// Add required field zipCode to each taxpayer
old.taxPayers.map<Nat, OldTaxPayer, NewTaxPayer>(
func(_id, oldTaxPayer) { { oldTaxPayer with zipCode = "" } }
);
新しいフィールドをオプション型にする場合は、null を設定します。
{ task with var status = task.status; var assignee = null : ?Principal }
バリアント型の追加または拡張
バリアントタグを追加する場合は、旧値を新しいバリアント型にキャストします。
var status = bounty.status : NewTaskStatus // OldTaskStatus had #open;#claimed;#completed; New adds #expired
Bool をバリアントに置き換える場合:
var status = if (task.completed) { #completed } else { #pending };
コレクションへのマッピング
map<K, OldV, NewV>(func(key, oldVal) { ... }) を使って、Map の各値を変換します。
let todos = old.todos.map<TaskID, OldTask, NewTask>(
func(_, task) {
{
task with
due = 0;
var status = if (task.completed) { #completed } else { #pending };
};
}
);
{ var todos; var nextTaskId = old.nextTaskId };
新しいトップレベル安定フィールドの追加(旧状態から計算)
マイグレーション出力に新しいフィールドを含め、旧フィールドから計算します。
let times = List.repeat<Time.Time>(0, old.messages.size());
{ var messages = old.messages; var times };
再構成:状態の形状を変更
新しい状態の構造が異なる場合は、旧フィールド型から新しい型への変換関数を用意し、アプリケーションのマイグレーションロジックに従って実装します。マイグレーション内で旧状態から新しい値を作成し、NewActor に一致するレコードを返します。
フィールドの型を変更
名前または概念の変更 — 例:artist : Text から artists : Set.Set<Text> への変更:
{ oldPainting with artists = Set.singleton<Text>(oldPainting.artist) }
1 つのフィールドの名前または型を変更
入力レコードでは旧名と旧型を使用し、出力レコードでは新名と新型を使用します。
// src/backend/migrations/YYYYMMDD_HHMMSS.mo
import Int "mo:core/Int";
module {
type OldActor = { var state : Int };
type NewActor = { var newState : Float };
public func migration(old : OldActor) : NewActor {
{ var newState = old.state.toFloat() };
};
};
フィールドを意図的に削除
マイグレーション入力でフィールドを受け取りますが、出力には含めません。コンパイラ警告が出るため、データの損失が意図したものであることを確認してください。
新しいフィールドの追加
main.mo にフィールドを型のみで追加し(初期化子は付けません)、次のマイグレーションの NewActor にも、既存のフィールドから計算した値または定数を指定して追加します。強化されたマイグレーションでは、アクター本体で初期値を指定できません。
マイグレーション時の動作
- マイグレーション関数はアップグレード時に、アップグレードコンテキストで一度だけ実行されます。マイグレーション本体の
Time.now()などが返す値は、各レコードが作成された時刻ではなく、アップグレード時刻です。 - マイグレーションでは定数やマジック値を使わないでください。マイグレーション入力、または明確に定義されたアプリケーションの型からデフォルト値を導出してください。
互換性とツール
アップグレード安全性チェックでは、新しいアクター本体と、最後にデプロイされた安定シグネチャを比較します。安定シグネチャは .most ファイル(安定シグネチャのエンコードされたスナップショット — フィールド名と型)で、mops.toml の [canisters.<name>.check-stable] から参照されます。ここに設定されている場合、mops check --fix が自動的に読み込みます。ホスト型プラットフォームでは通常、この設定が行われ、前回の .most ファイルがプロジェクトとともに保持されます。
チェックに合格するには、新しいマイグレーションの OldActor が以前にデプロイされたシグネチャと一致し、NewActor が新しいアクター本体と一致する(または安定したスーパータイプである)必要があります。
OldActor を確実に導出するには、OldActor を src/backend/migrations/ 内で自分のファイルより前にあるファイル(辞書順)の NewActor と同じにします。自分のファイルがチェーンの最初のファイルであれば {} にします。{} が適用されるのは、チェーンが空のカニスターから始まる場合のみです。レガシー永続化から変換されたプロジェクトは、変換前の安定した形から始まり、既存の最初のファイルにはすでにその形が反映されています。
アップグレードのチェックリスト
- 暗黙的(互換性のある変更)か明示的(新しいマイグレーションファイル)かを決めます
- ビルドごとに保留中のマイグレーションファイルは最大1つです。 ファイルを作成する前に
src/backend/migrations/を確認してください。すでに保留中のマイグレーションがある場合は、2つ目を追加せず、既存のファイルを編集します - 明示的な場合は、既存のすべてのファイルより後にソートされる、UTC タイムスタンプのみのファイル名(
YYYYMMDD_HHMMSS.mo、接尾辞なし)を選びます。変更内容をファイル名に含めないでください -
OldActorは、src/backend/migrations/内で自分のファイルより前にあるファイル(辞書順)のNewActorと同じにします。チェーンのあるプロジェクトの最初のファイルであれば{}にします。現在のmain.moを使わないでください。ファイル自身のNewActorも使わないでください - ディレクトリが空(初期マイグレーション)の場合、
NewActorにはmain.moで宣言されているすべての安定フィールドと、それぞれの値を含める必要があります -
OldActor(古い型)とNewActor(新しい型)を両方ともインラインで定義します。プロジェクトのインポートは使いません -
public func migration(old : OldActor) : NewActorを実装します - 適用済みのマイグレーションは変更も削除もしないでください。保留中のマイグレーションは重複して追加せず、編集します
- データのマイグレーションに preupgrade/postupgrade や
(with migration = ...)を使わないでください - 合格するまで
mops check --fixを繰り返し実行します(高速)。コンパイルとアップグレードの安全性を検証します - 最後に
mops buildを1回実行します(低速)。バックエンドをコンパイルし、更新された IDL バインディングを生成します
参考情報
- マイグレーションの例:最小限の自己完結した例(フィールドの追加、コレクションのマッピング、オプションフィールドの追加)については、examples.md を参照してください
- 説明がつかない問題がある場合:説明できない互換性の診断結果、凍結されたファイルへの書き込み失敗、レガシー永続化から変換されたプロジェクト、チェーンの削除依頼については、
troubleshooting-motoko-migrationsを読み込んでください。通常の手順では不要です - Motoko 開発全般:言語の基礎、コアライブラリのリファレンス、アーキテクチャのパターンについては、
writing-motokoを使用してください - mops ツール:
mops.tomlの設定、mops check、mops build、ツールチェーンのセットアップについては、mops-cliを読み込んでください - Motoko Docs: データ永続化
- Motoko Docs: 互換性