概要
ページ削除処理が Eloquent モデルを経由しない生の DELETE 文で実装されているため、削除された親ページを指したままの parent_id を持つ子ページが残ります。
app/Plugins/Manage/PageManage/PageManage.php
public function destroy($request, $page_id)
{
// Log::debug($id);
DB::table('pages')->where('id', '=', $page_id)->delete();
// ページ管理画面に戻る
return redirect("/manage/page");
}
DB::table()(クエリビルダ)による削除のため、NodeTrait(kalnoy/nestedset)の削除フックが動作しません。本来 Page::find($page_id)->delete() であれば行われる以下の処理がすべてスキップされます。
- 子孫ページの連鎖削除
- 残ノードの
_lft/_rgt の詰め直し
- 子ノードの
parent_id の付け替え
結果として起きること
| 項目 |
削除後の状態 |
子ページの parent_id |
削除された親のIDを指したまま残る(不整合) |
子ページの _lft/_rgt |
番号に穴が空くのみ。祖先判定は消えた親を飛ばして解決されるため実用上は正しく動作する |
| 子ページ自体 |
削除されず残り、実質トップレベル扱いになる |
ancestorsAndSelf() は _lft/_rgt の範囲比較で祖先を引くため、行ごと消えた親は自然に候補から外れます。そのため _lft/_rgt 由来の情報は正しく、parent_id だけが取り残されるという非対称な状態になります。
この不整合自体は長らく無害でしたが、parent_id を参照する機能が追加されたことで顕在化しました。
現状の把握
- この
destroy() は initial commit から変更されておらず、初期から存在する挙動です
- ある顧客環境では系統の異なる5件の不整合が確認されており、単発ではなく繰り返し発生しています
- 子ページを持つページも警告なく削除できます(
page_edit.blade.php の削除フォームに子の有無チェックはなく、確認ダイアログのみ)
pages テーブルを生クエリで削除・更新している箇所は、この1箇所のみです
実行環境
OS: 影響なし(サーバサイドの実装によるもののため全環境で再現)
ブラウザ: 影響なし
Connect-CMS: 全バージョン(initial commit から現在まで)
再現手順
- 親ページAの配下に子ページBを作成する
- ページ管理から親ページAを削除する
- DBを確認すると、子ページBの
parent_id が削除済みの親ページAのIDを指したまま残っている
- 子ページB自体は削除されず、ページ一覧上はトップレベルのページとして扱われる
対応候補
方針は未定です。以下は候補の列挙にとどめます。
destroy() を Page::find($page_id)->delete() に変更する
- ただし子孫ページが連鎖削除される挙動変更になります。現状の「子が残る」挙動に依存した運用がある可能性があり、要件確認が必要です
- 子ページを持つページの削除時に、警告表示またはブロックを行う
- 既存データの
parent_id を _lft/_rgt から再同期するメンテナンスコマンドを提供する
注意
Page::fixTree() は parent_id を正として _lft/_rgt を再構築するメソッドです。今回の壊れ方(parent_id のみが不正)に対して実行すると、正しい _lft/_rgt を不正な parent_id で上書きするため状況が悪化します。復旧には逆方向(_lft/_rgt → parent_id)の処理が必要です。
本Issueが原因となって顕在化した不具合: #2480
概要
ページ削除処理が Eloquent モデルを経由しない生の DELETE 文で実装されているため、削除された親ページを指したままの
parent_idを持つ子ページが残ります。app/Plugins/Manage/PageManage/PageManage.phpDB::table()(クエリビルダ)による削除のため、NodeTrait(kalnoy/nestedset)の削除フックが動作しません。本来Page::find($page_id)->delete()であれば行われる以下の処理がすべてスキップされます。_lft/_rgtの詰め直しparent_idの付け替え結果として起きること
parent_id_lft/_rgtancestorsAndSelf()は_lft/_rgtの範囲比較で祖先を引くため、行ごと消えた親は自然に候補から外れます。そのため_lft/_rgt由来の情報は正しく、parent_idだけが取り残されるという非対称な状態になります。この不整合自体は長らく無害でしたが、
parent_idを参照する機能が追加されたことで顕在化しました。現状の把握
destroy()は initial commit から変更されておらず、初期から存在する挙動ですpage_edit.blade.phpの削除フォームに子の有無チェックはなく、確認ダイアログのみ)pagesテーブルを生クエリで削除・更新している箇所は、この1箇所のみです実行環境
再現手順
parent_idが削除済みの親ページAのIDを指したまま残っている対応候補
方針は未定です。以下は候補の列挙にとどめます。
destroy()をPage::find($page_id)->delete()に変更するparent_idを_lft/_rgtから再同期するメンテナンスコマンドを提供する注意
Page::fixTree()はparent_idを正として_lft/_rgtを再構築するメソッドです。今回の壊れ方(parent_idのみが不正)に対して実行すると、正しい_lft/_rgtを不正なparent_idで上書きするため状況が悪化します。復旧には逆方向(_lft/_rgt→parent_id)の処理が必要です。本Issueが原因となって顕在化した不具合: #2480