migrating-motoko-actors について
mops 管理の移行チェーンを使う Motoko canister の開発者向けスキルです。暗黙の互換性で足りる場合と、移行履歴を保ちながら状態変換を明示的に記述する方法を説明します。
主な機能
- 01暗黙的移行と明示的移行の区別
- 02タイムスタンプ付き移行ファイルの規則
- 03安定状態と Actor 宣言の説明
- 04状態変換パターンの提示
- 05mops による互換性検査の説明
活用シーン
- 01安定状態を変更する canister のアップグレード計画
- 02アップグレードでフィールドを追加または改名
- 03保存済みレコードやコレクション値の変換
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill migrating-motoko-actorsインストール後、Claude Code で/migrating-motoko-actorsを入力して読み込まれたか確認します。
同梱ファイル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も警告しません。移行は少ないほど常に優れています。Identity body = 移行不要です。
(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 フィールドは通常どおり初期化子を使用します。
この初期化子なしのルールは、エンハンスドマイグレーションプロジェクト(このスキルの対象)に適用されます。この場合、初期値はマイグレーションチェーンが管理します。エンハンスドマイグレーションチェーンを使わない通常のプロジェクトでは、通常どおりアクター本体内で安定フィールドをインライン値で初期化します(例:
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 について:
- 新シグネチャに、
TがUの安定サブタイプである安定フィールドid : 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) }
単一フィールドの名前または型を変更する
入力レコードでは旧名/旧型を使用し、出力レコードでは新名/新型を使用します。
// 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()などの値は、各レコードが元々作成された時刻ではなく、アップグレード時刻を表します。 - マイグレーションでは定数やマジック値を使わないでください。マイグレーション入力または明確に定義されたアプリケーション型からデフォルト値を導出してください。
互換性とツール
アップグレード安全性チェックでは、新しいアクター本体を、最後にデプロイされた安定シグネチャと比較します。安定シグネチャとは、mops.toml の [canisters.<name>.check-stable] から参照される .most ファイル(安定シグネチャのエンコード済みスナップショットで、フィールド名と型を含みます)です。これが設定されている場合、mops check --fix が自動的に検出します。ホスティングプラットフォームでは通常、この設定が済んでおり、以前の .most もプロジェクトと一緒に保持されています。
チェックに合格するには、新しいマイグレーションの OldActor が以前にデプロイされたシグネチャと一致し、NewActor が新しいアクター本体と一致する(またはその安定スーパータイプである)必要があります。
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を使用してください - Motoko Docs:データの永続化
- Motoko Docs:互換性