Obsidianで親ノートのIDを移すとき、保存前の検査と途中失敗を分ける

シリーズ「ObsidianのZettel IDプラグイン」の記事

僕は、Obsidianで一つの焦点に絞った考えをZettelとして残し、ノート先頭のメタデータ(front matter)に 11.a1.a.1 のようなIDを付けています。この階層型のZettel IDは、どの考えから派生したかを親子関係として表すものです。

「Obsidianの階層Zettel IDを移すときは、子孫IDの対応表を先に作る」では、親だけを書き換えず、子孫を含む新旧IDの対応表を先に作る判断を書きました。「ObsidianのZettelノートを、自分や子孫の下へ移動できないようにした」では、循環と指定IDの衝突を保存前に拒否する境界を扱いました。

この記事では、この2つを自作プラグインの一つの更新処理としてつなぎます。扱う「安全」は、子孫までの変更先を先に計算し、実行前に分かる循環と衝突を拒否する範囲です。複数ノートの保存を一つの処理として確定する仕組みや、途中失敗からの自動復元は含みません。

まず全体像

更新は、対象の部分木を集め、全ノートの変更先を決め、拒否条件を確認してから1件ずつ保存します。保存前の検査と保存中の失敗は、別の段階として扱います。

Zettel IDの部分木を更新する4段階 親と子孫を集め、新旧IDの対応表を作り、循環と衝突を検査してから、ノートを1件ずつ保存する。保存途中に失敗した場合は自動復元しない 1. 部分木を集める 1 / 1.a / 1.a.1 2. 対応表を作る 1 → 3 / 1.a → 3.a 3. 先に検査する 循環 / 指定IDの衝突 4. 保存 1件ずつ反映 途中失敗:新旧IDが混ざる 自動復元せず、バックアップと対応表で照合
保存前に分かる不正な移動は止められます。一方、保存開始後の失敗では一部だけ変更されるため、復旧の準備を更新処理とは別に持ちます。図は横へスクロールできます。

子孫までの対応表を先に確定する

例として、1の部分木を3へ移す場合を考えます。プラグインは現在のツリーから親と子孫を集め、保存前に次の対応表を作ります。

ノート 変更前 変更後
1 3
1.a 3.a
1.a.1 3.a.1

実装では、子ノートをツリーの表示順にたどり、新しい親IDの末尾に応じて数字と英字を交互に採番します。移動対象の外に同じ子IDがあれば、そのIDを上書きせず、次の空きIDへ進みます。

この計算を先に終えることで、保存中に次のIDを考え直す必要がなくなります。ただし、対応表を作れたことは、すべての保存が成功する保証ではありません。

保存前に拒否できる条件を分ける

プラグインが保存前に止めるのは、実行前のツリーとID一覧から判断できる条件です。

  1. 自分自身、または自分の子孫を移動先の親にする
  2. 利用者が指定した新しいルートIDが、移動対象外のノートで使われている

1つ目は、親をたどると元のノートへ戻る循環を作ります。2つ目を許すと、別のノートと同じIDを持つ衝突になります。どちらも対応表をノートへ書く前に拒否するため、この段階では一部更新は起きません。

ドラッグ操作で移動先の親だけを選ぶ場合は、プラグインが空いている子IDを自動採番します。利用者が新IDを直接指定する操作では、そのIDが使用中なら拒否します。

逐次書き込みの限界

対応表の検査が終わると、プラグインは各ノートのfront matterを更新し、保存完了を待ってから次のノートへ進みます。全件成功か全件取り消しのどちらかに揃える仕組みはありません。

たとえば親13へ保存した直後、子1.aの保存で失敗すると、親だけが3、子孫は1.a以下のまま残ります。エラーは表示されますが、先に保存した親は自動で1へ戻りません。

同じ操作を再実行しても、元の部分木全体を直せるとは限りません。現在のIDからツリーを作り直すと、3の下に古い1.aは含まれず、再実行の対象から外れるためです。

失敗後は再実行より先に照合する

現行実装では、対応表を保存前にメモリ上で作り、各ノートへ1件ずつ書き込みます。ファイルから新IDを指定する操作では、変更前後のIDを記録した結果ファイル(JSON)を全ノートの保存後に出力します。そのため、途中失敗時に対応表を復旧用として残す処理も、自動で巻き戻す処理もありません。

このため、僕は実行前後を次の順で扱います。

  1. Vault(Obsidianのノート保管場所)をバックアップする
  2. 対象の部分木を小さくし、変更前のIDを確認する
  3. 移動を実行する
  4. 変更後にIDの重複と、予定した親子関係を照合する
  5. 途中失敗なら同じ操作を繰り返さず、バックアップと現在値を比べて手動修復を判断する

自動ロールバックは、戻す処理自体が途中で失敗する場合まで扱わなければ安全とは呼べません。現状のプラグインではそこまで実装していないため、この記事でも復旧を自動化できるとは書きません。

架空ノートで確認した範囲

実際のVaultを使わない架空ノートでは、保存前の循環・指定ID衝突で書き込みが始まらないこと、親から孫までの対応表が先に作られることを確認しました。保存の2件目で意図的に失敗させると、親だけが新IDになり、子孫が古いIDのまま残ります。

その状態から親ノートの操作を再実行しても、古いIDの子孫は対象へ戻りませんでした。同じ架空ノートの確認では、保存前に作ったファイル単位の対応表を別に保持すれば、現在値と照合して残りを修復できることも確かめました。ただし、現行プラグインは失敗前にその表をファイルへ残さないため、実運用の復旧元は事前バックアップです。

これは実Vaultでの自動復旧テストではなく、逐次保存と再実行の限界を分けるための確認です。対応表を保存前に外へ残す処理は、今後の改善点として残っています。

まとめ

階層型Zettel IDの変更では、子孫までの対応表を保存前に確定し、循環と指定IDの衝突を先に拒否します。これで実行前に分かる不正な移動は止められます。

それでも、1件ずつ保存する途中で失敗すれば、新旧IDが混ざります。同じ操作の再実行を復旧手段と考えず、現在の実装ではバックアップと変更前のIDを使って照合するところまでを運用に含めます。