メインコンテンツにスキップ

合同会社recipe

ConoHa WINGでリストアしたDBが一覧に出てこない ── 表示件数の壁を越えて接続許可を付けるまで

こんにちは、合同会社recipeです。
今回は、ConoHa WING の自動バックアップからデータベースをリストアしたのに、復元した DB が管理画面のどこにも出てこない──という状況にハマり、脱出するまでの記録です。

結論から言うと、原因は「ConoHa WING のコントロールパネルが DB・DB ユーザーの一覧を先頭 100 件しか描画しない」ことでした。DB を数百個運用しているアカウントだと、リストアで作られた DB も、それに権限を付けたいユーザーも、一覧に出てきません。同じ壁に当たった方の助けになれば幸いです。

◆ 何が起きたか

ある WordPress サイトで、投稿に紐づくカスタムフィールド(wp_postmeta の値)が 30 件まとめて空になっていることに気づきました。WordPress のリビジョンはカスタムフィールドを保存しないので、リビジョンからは戻せません。頼りになるのは サーバーの自動バックアップ だけです。

ConoHa WING には 14 日分の自動バックアップがあり、「サーバー管理 → 自動バックアップ」から日付を選んで DB をリストアできます。ここまでは公式の手順どおりで、リストア履歴にも「完了」と出ました。

ところが、サイトを見ても何も変わっていない。8 日分、7 日分、さらに遡って 2 日分と 3 回リストアしても、カスタムフィールドは空のままでした。

◆ ConoHa WING の DB リストアは「別名の DB を作る」だけ

まず押さえておくべき仕様がこれです。ConoHa WING の DB リストアは、稼働中の DB を上書きしません。リストア履歴の説明にもこう書いてあります。

DBの場合は新たなデータベースを作成し、復元したデータを保存します。

つまり、xxxxx_mydb をリストアすると xxxxx_mydb_20260916_1 のような 日付+連番つきの別 DB が作られるだけで、WordPress はまだ元の DB を見ています。使うには次のどちらかが必要です。

  1. wp-config.php の DB_NAME を復元先の DB 名に書き換える(サイト全体をその時点に巻き戻す)
  2. 復元先 DB から必要な行だけを稼働 DB に SQL でコピーする(他のデータを巻き戻さない)

そして、どちらの方法でも DB ユーザーに復元先 DB への「接続許可」を手動で付ける 必要があります。リストアしただけでは、どのユーザーからも復元先 DB は開けません。phpMyAdmin のツリーにも出ませんし、SSH から mysql で SHOW DATABASES しても出てきません(MySQL は権限のない DB の存在を教えてくれないので、存在確認すらできません)。

◆ ハマった点:一覧が「先頭 100 件」で切れる

接続許可は「サイト管理 → データベース」のユーザー一覧から、対象ユーザーの「接続先データベース」を編集して付けます。ここで詰まりました。

私たちのアカウントは複数のサービスを載せていて、DB が 500 件・DB ユーザーが約 500 件 あります。コントロールパネルはこの一覧を 先頭 100 件しか描画しません。並び替え(作成日/ネームタグの昇順・降順)を変えても 100 件の中に入らなければ表示されず、ページ送りもありません。

試して駄目だったこと:

  • 並び替えを変える → 目的の行が 100 件に入らず表示されない
  • サイト管理の対象サイトを切り替える → 変わらず
  • SSH で mysql から GRANT → 一般ユーザー権限なので不可
  • 公式 API → ConoHa の API は VPS 向けで、WING には API メニュー自体がない

◆ 解決:管理画面が呼んでいる内部 API に limit を差し込む

コントロールパネルは、画面の裏で次のようなエンドポイントを呼んでいます(ブラウザの開発者ツール → Network で確認できます)。

用途メソッドパス
ユーザー一覧POST/Wing/DatabaseTabPageBG/FetchUsers
データベース一覧POST/Wing/DatabaseTabPageBG/FetchSchemas
接続 DB 変更(保存)POST/Wing/DatabaseTabPageBG/UpdateGrant

ポイントは、これらの API が limit / offset というページング用パラメータを受け付ける ことです。UI は limit を 100 で固定して呼んでいるだけで、大きな値を渡せば全件返ってきます。

方法A:UI に全件を描画させる(おすすめ)

一番安全なのは、パネル自身の API 呼び出しに limit を足して、パネル本来の処理で全件を描画させる やり方です。表示さえされれば、接続先 DB の変更もいつものレンチアイコンから普通に操作できます。

「サイト管理 → データベース」を開いた状態で F12 → Console に以下を貼り付けて実行します。

(() => {
  if (window.__limitPatched) return 'already patched';
  const XO = XMLHttpRequest.prototype.open;
  const XS = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function (m, u) { this.__u = u; return XO.apply(this, arguments); };
  XMLHttpRequest.prototype.send = function (b) {
    try {
      if (this.__u && (this.__u.indexOf('FetchUsers') >= 0 || this.__u.indexOf('FetchSchemas') >= 0)
          && typeof b === 'string' && b.indexOf('limit=') < 0) {
        b += '&limit=5000';
      }
    } catch (e) {}
    return XS.call(this, b);
  };
  window.__limitPatched = true;
  return 'patched';
})();

実行後、左メニューの「データベース」をクリックし直すと一覧が再取得され、DB もユーザーも全件 描画されます。あとは目的のユーザー行を展開し、「接続先データベース」のレンチアイコンから復元先 DB にチェックを入れて保存するだけです。

行が多くて探しにくいときは、続けてこれを実行するとスクロールしてハイライトしてくれます。

(() => {
  const name = 'xxxxx_mydb';   // ← 探したいユーザー名
  const lis = [...document.querySelectorAll('li.schema-detail, li.items__list')];
  const hit = lis.find(li => (li.textContent || '').trim().split(/\s|\n/)[0] === name);
  if (!hit) return 'not found';
  hit.scrollIntoView({ block: 'center' });
  hit.style.outline = '3px solid #ff5a00';
  return 'scrolled';
})();

注意:ページを再読み込み(F5)するとパッチが外れて 100 件表示に戻ります。表示されている間に操作を済ませてください。

方法B:API を直接叩いて確認だけする

「そのユーザーがどの DB に接続できるのか」を値で確認したいだけなら、Console から直接 fetch する方が早いです。CSRF トークンはページ内の hidden input から取れます。

(async () => {
  const serviceId = new URLSearchParams(location.search).get('serviceId');
  const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
  const body = `serviceId=${serviceId}&__RequestVerificationToken=${encodeURIComponent(token)}&limit=5000`;
  const h = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' };

  const [u, s] = await Promise.all([
    fetch('/Wing/DatabaseTabPageBG/FetchUsers',   { method: 'POST', headers: h, body, credentials: 'same-origin' }).then(r => r.json()),
    fetch('/Wing/DatabaseTabPageBG/FetchSchemas', { method: 'POST', headers: h, body, credentials: 'same-origin' }).then(r => r.json()),
  ]);

  const schemaById = Object.fromEntries(s.ResponseData.Data.ItemList.map(x => [x.SchemaId, x.SchemaName]));
  const user = u.ResponseData.Users.find(x => x.UserName === 'xxxxx_mydb');
  console.log('DB 総数:', s.ResponseData.Data.TotalCount, '/ ユーザー総数:', u.ResponseData.Users.length);
  console.log(user.UserName, '→', user.ConnectSchemaIds.map(id => schemaById[id]));
})();

レスポンスは概ねこんな形です。ConnectSchemaIds がそのユーザーの接続許可 DB、TotalCount が本当の件数です。

// FetchUsers
{ "Result": "success",
  "ResponseData": { "Users": [
    { "UserId": "…", "UserName": "xxxxx_mydb", "PhpMyAdminUrl": "…", "ConnectSchemaIds": ["…", "…"] }
  ] } }

// FetchSchemas
{ "Result": "success",
  "ResponseData": { "Data": {
    "ItemList": [ { "SchemaId": "…", "Nametag": "mydb", "SchemaName": "xxxxx_mydb", "Host": "mysqlNNNN.conoha.ne.jp", … } ],
    "TotalCount": 500 } } }

◆ 接続許可を付けたあと

ここまで来れば、あとは公式手順どおりです。

  1. phpMyAdmin(または SSH の mysql)にそのユーザーでログインし直すと、復元先 DB がツリーに出る
  2. サイト全体を巻き戻してよければ wp-config.php の DB_NAME を復元先の名前に変更(DB_USER はそのまま)
  3. 一部だけ戻したければ、復元先 DB と稼働 DB を JOIN して必要な行だけ UPDATE する

今回はカスタムフィールド 30 件分だけが目的だったので、本来は 3 番のような SQL が理想でした。

UPDATE xxxxx_mydb.wp_postmeta AS live
JOIN  xxxxx_mydb_20260916_1.wp_postmeta AS bk
  ON  bk.post_id = live.post_id AND bk.meta_key = live.meta_key
SET   live.meta_value = bk.meta_value
WHERE live.post_id IN (…対象の投稿ID…)
  AND live.meta_key IN ('field_a','field_b','field_c');

wp-config.php を書き換える方法を選ぶ場合は、変更前の値をコメントで残しておくと戻すのが楽です。

// define( 'DB_NAME', 'xxxxx_mydb' );              // 2026-09-17 変更前
define( 'DB_NAME', 'xxxxx_mydb_20260916_1' );      // 2026-09-17 変更後(9/8 バックアップ)

◆ 注意点

  • limit / offset は 非公式の内部パラメータ です。ConoHa 側の仕様変更で使えなくなる可能性があります
  • 自分のアカウント・自分のコントロールパネルに対する操作に限ってください。読み取りは Console から、接続許可の保存(UpdateGrant)は UI のボタンから 行うのが安全です
  • __RequestVerificationToken や各種 GUID を他人に貼らないでください
  • DB_NAME を切り替える方法は バックアップ以降の変更がすべて巻き戻ります。予約や会員データなど動きのあるテーブルがあるサイトでは、行単位のコピーを選んでください
  • 切り替え後、バックアップ日より後に発行した アプリケーションパスワード は新 DB に存在しないので、REST API を使うなら再発行が必要です

◆ おまけ:「いつ消えたか」はアクセスログで分かる

「どの日のバックアップに戻せばいいか」を決めるには、消えた日時が要ります。wp_postmeta には更新日時がないので DB からは分かりませんが、ConoHa WING は SSH を有効にすると ~/logs/<ドメイン>/access_log でアクセスログが読めます(約 7 日分)。

今回は「フォームの一括更新が空の値で 30 件走った」ケースだったので、admin-ajax.php への POST が分単位で固まっている箇所を探しました。

(zcat access_log.*.gz; cat access_log) \
  | grep "POST /path/wp-admin/admin-ajax.php" \
  | awk '{print substr($4,2,17), $1}' | sort | uniq -c | awk '$1>=5'

結果、ある日の 18:17 に 30 件の POST が 0.6 秒間隔で並んでいて、その直前に別の操作が 30 回──という形がそのまま出てきました。バックアップはその前日分を選べば十分、と判断できます。

◆ まとめ

  • ConoHa WING の DB リストアは 別名の DB を作るだけ。使うには接続許可の手動設定と、wp-config.php の変更または SQL コピーが必要
  • コントロールパネルの DB・ユーザー一覧は 先頭 100 件しか描画しない。DB を大量に持つアカウントでは復元先が見えない
  • 裏の API(FetchUsers / FetchSchemas)は limit を受け付けるので、Console からパッチを当てれば全件表示できる
  • 消えた日時はアクセスログの admin-ajax.php の連打から特定できる

「リストアしたのに何も変わらない」で数時間溶かした身としては、ConoHa さんには一覧のページ送りか検索窓が欲しいところですが、仕様が変わるまではこの方法でしのげます。同じ状況の方の参考になれば幸いです。