WordPress のデータベースURLをシリアライズデータを壊さずに置換する方法
ウィジェットやメニュー、テーマ設定を消さずにデータベース上のサイトURLを変更する。シリアライズデータを保ったまま置換できる WP-CLI コマンドを解説します。
公開
WordPress のデータベース全体に対して UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') を素のまま実行してはいけません。文字列は確かに正しく置き換わりますが、同時に設定が破壊されます。WordPress は配列やオブジェクトを PHP のシリアライズ文字列として保存しており、その形式は含まれる各文字列のバイト長を記録しているからです。生の SQL 置換はテキストを書き換えるだけで記録された長さには一切触れないため、その値は読み取れなくなります。
代わりにシリアライズ対応のツールを使ってください。コマンドラインなら wp search-replace です。まずデータベースをバックアップし、ドライランで確認してから本番実行します。
実際に壊れているもの
PHP のシリアライズ文字列はこういう形をしています。
s:18:"http://example.com"
18 は後続テキストのバイト長です。数えてみてください。http://example.com はちょうど 18 文字です。
保存済みのテーマ設定を想像してみましょう。
a:1:{s:4:"logo";s:18:"http://example.com";}
ここで http:// を https:// に素の SQL で置換すると、こうなってほしいところです。
a:1:{s:4:"logo";s:19:"https://example.com";}
しかし実際にはそうなりません。得られるのはこれです。
a:1:{s:4:"logo";s:18:"https://example.com";}
文字列は 19 バイトになったのに、18 バイトだと主張したままです。PHP は宣言された長さを読み、18 バイト先まで進み、そこにあるはずの閉じ引用符とセミコロンを見つけられず、処理を諦めます。配列全体のアンシリアライズが失敗します。
なぜ静かに失敗するのか
この挙動こそが、このバグの代償を高くしています。WordPress はエラーを投げて止まってくれません。オプションを読み込むときに値をアンシリアライズのヘルパーに通し、壊れたデータに対して PHP は false を返し、WordPress はそのオプションが最初から設定されていなかったかのような値をコードに渡します。プラグインやテーマはそこでデフォルト値にフォールバックします。
つまり症状はエラーページではありません。症状はこうです。
- ウィジェットがサイドバーから消える
- カスタマイザーの設定がテーマのデフォルトに戻る
- ページビルダーのレイアウトデータが空になり、ページが真っ白で表示される
- プラグインの設定画面がインストール直後の状態に見える
- メニューの表示位置の割り当てが外れる
管理画面には異常を示すものが何もありません。行はデータベースに残ったままで、一見正しく見えるテキストで満たされています。ずれているのは長さのプレフィックスだけ、それも 1〜2 バイトです。アンシリアライズに失敗すると PHP は通知を出しますが、エラー表示をオフにした本番サイトでは誰の目にも入りません。WP_DEBUG を有効にし、WP_DEBUG_LOG をオン、WP_DEBUG_DISPLAY を false にすれば、その通知を読めるログファイルに書き出せます。
被害の広がり方も均一ではありません。単純なスカラー値、たとえば siteurl、home、素の投稿本文、独立したカラムに入っている URL などは、雑な置換でも問題なく生き残ります。壊れるのはシリアライズされた値だけです。だからこそ、失敗した置換は一見うまくいったように見えるのです。
何かに手をつける前に wp-config.php を確認する
wp-config.php で WP_HOME や WP_SITEURL が定義されていると、その定数がデータベースの値より優先されます。wp_options を書き換えても、まったく何も起きていないように見えます。データベースが原因だと結論づける前に、こういう行がないか確認してください。
define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );
新しいURLに更新するか、削除してデータベース側に任せるかのどちらかです。
バックアップは本気で取る
シリアライズの破損は、必ずしも簡単に元へ戻せるものではありません。長さのプレフィックスと中身が食い違ってしまうと、壊れた値から元の値を復元することはできません。元の文字列を知らない限り、正しい長さがいくつだったのかは分からないからです。実質的な取り消し手段はバックアップだけです。
wp db export backup-before-replace.sql
WP-CLI が使えない場合は mysqldump で。
mysqldump -u USER -p DBNAME > backup-before-replace.sql
可能ならサーバー外に保管してください。ウェブルートに置かれたバックアップは、復旧ポイントであると同時にセキュリティ上の問題でもあります。
安全なコマンド
WP-CLI の search-replace は各値をアンシリアライズし、デコードした構造をたどって内部で置換し、正しい長さを付け直して再シリアライズします。まずドライランから。
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
出力されたレポートを読んでください。テーブルごと、カラムごとに何件置換されるかが示されます。テーブル一覧に違和感があったり、見覚えのないテーブルが何千件もヒットしていたりしたら、本番実行の前に止まって中身を確認してください。
問題なければ実行します。
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
フラグについて、正直なところを補足します。
--all-tablesは WordPress 自身が登録していないテーブルまで対象にします。プラグインのデータはそこに置かれていることが多く、付けないと一部のプラグインテーブルが丸ごとスキップされます。--skip-columns=guidは重要です。guidはフィードリーダー向けの恒久的な識別子であり、実際に使われるURLではありません。書き換えると、購読者にはアーカイブ全体が新着記事として見えてしまうことがあります。- 検索文字列は末尾のスラッシュを付けず、触りたくないプロトコルの表記も含めないでください。
example.comだけで検索すると、メールアドレスや本文中の言及まで巻き込みます。
コマンドラインが使えない場合は、シリアライズ対応の移行プラグインが管理画面から同じデコード処理を行ってくれます。探すべきは「シリアライズデータを扱える」と明示されている挙動です。そう書かれていないツールは、対応していないと考えてください。
関係するテーブルとカラム
| テーブル | カラム | シリアライズの危険度 |
|---|---|---|
| wp_options | option_value | 高 — ウィジェット、テーマ設定、トランジェント、大半のプラグイン設定 |
| wp_postmeta | meta_value | 高 — ページビルダーのレイアウト、カスタムフィールドの配列 |
| wp_usermeta | meta_value | 高 — 権限、画面やダッシュボードの表示設定 |
| wp_termmeta | meta_value | 中 — プラグイン次第 |
| wp_posts | post_content, guid | 本文は低いが、ショートコードの属性やブロックのマークアップにURLが含まれる |
| wp_comments | comment_content, comment_author_url | 低 |
| wp_links | link_url, link_image | 低。そもそも未使用のことが多い |
マルチサイトではこれに加えて wp_blogs、wp_site、wp_sitemeta、さらにサイトごとの wp_2_options、wp_3_options などが対象になります。マルチサイトのURL変更は検索置換だけでは済まない追加ルールがあるので、別作業として扱ってください。
search-replace だけでは解決しないケース
限界についても正直に書いておきます。
データベース内に保存された JSON。 一部のページビルダーはレイアウトデータをスラッシュをエスケープした JSON で保存するため、URL が https://example.com ではなく https:\/\/example.com の形で入っています。通常の表記で検索しても引っかかりません。エスケープされた文字列に対してもう一度置換をかける必要があるかもしれません。必ずコピー環境で試してください。
Base64 でエンコードされた値。 プラグインが設定をエンコードしてから保存している場合、テキストベースの置換ではURLを見つけることすらできません。この種の設定は、プラグイン自身の画面から入力し直すのが基本です。
ファイルに直書きされたURL。 functions.php や子テーマのテンプレート、ハードコードされたアセットのパスに書かれたものはデータベースにないため、置換の対象外です。テーマディレクトリは別途 grep してください。
キャッシュと生成済みの出力。 オブジェクトキャッシュ、ページキャッシュ、CDN のコピーは、正しく置換した後も古いURLを配信し続けます。置換が失敗したと判断する前に、まずそれらをクリアしてもう一度確認してください。
すでに危険なクエリを実行してしまったら
バックアップから復元してください。それが答えです。ほとんど機能しない修復手順を提示するより、はっきりそう言うほうが誠実です。
バックアップがない場合、復旧できる範囲は限られます。シリアライズされていなかった値は無事ですし、壊れたシリアライズ値は手作業で組み直すか、該当する設定画面を保存し直してリセットするしかありません。ウィジェットエリアを保存し直す、メニューの表示位置を選び直す、プラグインの設定を入力し直す、といった操作は、壊れた値の上に正しくシリアライズされた新しい値を書き込みます。手間はかかりますが確実に効きますし、壊れた行を放置して次にサイトを触る人を混乱させるよりはるかにましです。
実行前に正しいステートメントを組み立てたい場合は、WordPress 検索置換 SQL ツールで生成してください。素の SQL で触っても安全なカラムと、シリアライズ対応の処理が必要なカラムがそれぞれ示されるので、コマンドを確定する前にその線引きを目で確認できます。
FAQ
よくある質問
単純な SQL の REPLACE でサイトが壊れるのはなぜですか?
WordPress は配列やオブジェクトを PHP のシリアライズ形式のテキストとして保存しており、その形式は中に含まれるすべての文字列のバイト長を記録しているためです。生の REPLACE は文字列だけを書き換え、古い長さの数値をそのまま残します。すると PHP はその値をアンシリアライズできず、WordPress はデフォルト値にフォールバックするため、設定は「間違った値」ではなく「空」になって現れます。
データベース上のサイトURLを変更する最も安全な方法は?
まずデータベースの完全なバックアップを取り、WP-CLI の wp search-replace をドライランで実行してから本番実行します。WP-CLI は各値をアンシリアライズし、デコードした構造の内部で置換したうえで、正しい長さを付け直して再シリアライズします。コマンドラインが使えない場合は、シリアライズ対応をうたう移行プラグインが管理画面から同じ処理を行います。
古いURLはどのテーブルに入っていますか?
主に wp_options、wp_posts、wp_postmeta、wp_usermeta、wp_termmeta、wp_comments です。シリアライズの危険が潜むのは wp_options と meta_value カラム全般で、ウィジェット、テーマ設定、トランジェント、ページビルダーのデータが配列として保存されています。マルチサイトでは wp_blogs、wp_site、wp_sitemeta にもドメインが入ります。
wp-config.php でURLを定義している場合、データベースを変えれば直りますか?
直りません。wp-config.php に WP_HOME や WP_SITEURL が定義されていると、その定数が wp_options の siteurl と home より優先されます。定数を更新または削除するまで、データベースを書き換えても表示は一切変わりません。移行がうまくいかない原因の多くはこの一行なので、まず wp-config.php を確認してください。
guid カラムも置換すべきですか?
置換しないでください。guid はフィードリーダー向けの恒久的な識別子であって実際に動作するURLではなく、書き換えるとフィードの購読者に過去の記事がすべて新着として再表示されるおそれがあります。WP-CLI の実行時は skip-columns で guid を指定してください。例外は、公開フィードも購読者も一度も存在しなかったサイトくらいです。
誤った置換の後、値が壊れているかどうかはどう判断しますか?
壊れたシリアライズ値はデータベース上では正常に見えるのに、WordPress 側では空で返ってきます。ウィジェットが消え、カスタマイザーの設定が初期化され、プラグインの設定がエラーもなくデフォルトに戻ります。WP_DEBUG と WP_DEBUG_LOG を有効にするとアンシリアライズの通知が記録されます。宣言された文字列長と実際のバイト数を突き合わせれば確実に確認できます。