NGW開発BLOG
← 記事一覧へ
·#079

phpMyAdmin も、いらなくなった本当に要る機能だけが残った

一ヶ月前に造った自作のSQLツールに、行の編集と削除を足した。カレンダーはブラウザ標準を捨てて自分で描いた。日付列を数えたら「0000-00-00」が974万件あったからだ。SQLを初めて書いたオフコンとORACLEの頃から、一列にカンマ区切りで持つFIND_IN_SETまで。要る機能だけを足していったら、phpMyAdminを開かなくなった。

一ヶ月で、開く道具が変わった

NGWSqlAdmin を造ったのは7月12日だ。DB切替、テーブル一覧、内容確認、SQL実行。4つで始めた。使いながら、欲しくなったものだけを足していく。そう決めて始めた道具である。

一ヶ月経った。気がつくと、phpMyAdmin を開く回数がゼロになっていた。

意図してそうしたわけではない。要るものを足していったら、そうなっただけだ。何を足したかを書く前に、少し昔の話をしたい。

SQLを初めて書いたのは、オフコンだった

私が最初に触ったSQLは ORACLE である。オフコンの時代だ。端末に向かって SELECT と打つと、表が返ってくる。それまで一行ずつ読んで組み立てていたものが、一文で返ってくる。あれは衝撃だった。

そこから PostgreSQL、MySQL、MariaDB と渡り歩いた。データベースを乗り換えるたびに方言の差に苛立ち、そのたびに「結局どれも SQL だ」と思い直す。核が同じだから乗り換えられる。標準化とはこういうことか、と後から分かった。

NGW は MySQL を選んだ。ORACLE の配下に入る前のことだ。 当時の選択としては素直だった。速く、軽く、情報が多い。

心配していることがある。以前は素朴に「いつか ORACLE が MySQL をやめると言い出さないか」と思っていた。だが技術者の間で実際に語られているのは、そこではないらしい。廃止よりも、オラクル製品としての囲い込みの方だ。

MySQL には無償の Community 版と、有償の Enterprise 版がある。同じ名前の製品に、線が一本引かれている。運用で効く機能——監査、バックアップ、暗号化まわり——は有償側にある。開発の重心も、クラウドで動かす製品の方へ寄っていく。やめるとは言わない。ただ、無償で使える範囲の線が、少しずつ動く。

閉じたネットワークの中で、自前のハードで動かしているこちらには、クラウド側の進化は一切届かない。届かないまま、線だけが動く。廃止のような分かりやすい形では来ない。気づいたときには、要る機能が向こう側にある。 そういう性質の懸念だ。

救いは、すでに出ているものが GPL であることだ。後から取り上げることはできない。 MariaDB が分かれられたのも、Percona が続いているのも、根拠はそこにある。逃げ道が制度として確保されている、という言い方の方が正確だろう。

ただ、稼働中の団体すべてのDBを移すとなれば、逃げ道と呼ぶには重い。今すぐの話ではない。二十年使う道具を選ぶときに、こういう見立ては持っておく、という程度の話である。

クエリと呼ばずに、SQLと呼ぶ

前から気になっていることがある。私たちはあれを「クエリを書く」とは言わず、「SQLを流す」と言う。なぜだろう。

SQL は Structured Query Language の略だ。直訳すれば「構造化された問い合わせ言語」。元は IBM が1970年代に作った SEQUEL(Structured English Query Language)で、商標の都合で SQL になった。名前の真ん中に Query が入っている。

だが実際に書くもののうち、問い合わせは一部でしかない。

  • 作る(CREATE / ALTER)
  • 入れる(INSERT)
  • 変える(UPDATE)
  • 消す(DELETE / DROP)
  • 問う(SELECT)

問い合わせは5つのうち1つだ。 名が体を表していない。

それでも私たちが「SQLを流す」と言うのは、たぶん、流すものが問い合わせだけではないと手が覚えているからだ。SELECT は読むだけで済む。それ以外は戻せない。名前は「問い合わせ言語」でも、キーボードを叩く指先が知っているのは、戻せないものを流しているという感触の方である。

この感触が、今回足した機能の設計をだいたい決めた。

一覧をクリックして、直せるようにした

足した機能の本命は、一覧の行をクリックすると編集できることだ。全列が並んだ画面が開き、値を直して保存する。行ごと削除もできる。

NGW のデータベースはビューは使うが、トリガもストアドプロシージャも使わない方針で設計してある。裏で何かが連鎖して走ることがない。だから1行直せば、それがそのまま最終状態になる。管理者が直接直すことの意味が大きい設計になっている。

意味が大きいということは、間違えたときの被害も大きいということだ。守りを4つ入れた。

行の特定は主キーだけに限る。 主キー(無ければ全列が NOT NULL の一意索引)が無いテーブルは、編集させない。理由を画面に出して止める。

phpMyAdmin は主キーが無いとき「全列の値が一致する行」を条件にする。あれは賢いやり方だが、同じ値の行が2つあれば両方に効く。 採らなかった。

書く直前に、対象が1件であることを必ず確かめる。 COUNT(*) を取って1でなければ何もしない。0件なら誰かが先に消している。複数件ならキーが効いていない。どちらも止めるべき場面だ。

キー列は画面で固定する。 書き換えると WHERE が別の行を指す。加えて NGW のデータは ID で表と表が繋がっているから、キーを変えれば参照が孤立する。直したければ SQL を書けばいい。

触っていない列は SET 句に入れない。 変更した列だけを送る。開いただけの列に、同じ値を上書きしない。

画面のセルは200字で切って表示している。だから編集を開くときは、必ずデータベースから取り直す。「…」で切れた値をそのまま保存するのが一番ありそうな事故だった。

カレンダーは、結局自分で描いた

日付の欄にカレンダーを付けたくなった。ブラウザには <input type="date"> がある。1行で済むはずだった。

済まなかった。実機で二つ起きた。

  • カレンダーのアイコンが描画されない。空の箱が並ぶ
  • ポップアップが閉じない

二つ目が厄介だった。調べると、ネイティブのカレンダーには閉じるための命令が存在しない。閉じる契機は「その要素からフォーカスが外れたとき」で、そこはブラウザが握っている。こちらから閉じる手段がない。

しかも私の手元では再現しなかった。ヘッドレスのブラウザでは正常に描画され、正常に閉じる。確かめられないものを、当てずっぽうで直し続けることになる。 二回それをやって、やめた。

実機では両方まだ駄目ですね。ネイティブのウィジェットは私の側から検証できず、閉じる制御もブラウザに握られたままです。当てずっぽうで直し続けるのは筋が悪いので、カレンダーを自前で描く方式に切り替えます。

自前で描けば、閉じる条件を自分で書ける。外側クリック、Escape、日付選択、再クリック、スクロール、リサイズ。明示できるものは、検証もできる。 実際に18項目をブラウザで動かして確かめた。

標準の部品を使うのは正しい。だが「標準だから安心」ではなかった。自分で確かめられる範囲まで降りるという判断の方が、この場合は正しかった。

数えたら、0000-00-00 が974万件あった

カレンダーを付けるにあたって、実データを数えた。日付型の列162本のうち、106本に 0000-00-00 が入っていた。合わせて974万件。

これは MySQL が古くから許してきた「ゼロ日付」だ。未設定を表すのに使われてきた。そして <input type="date"> は、この値を持てない。

置き換えていたら、その欄は空白に見える。空白のまま保存すれば、元の値とは違うものが書き込まれる。974万件のうち一件でもそれをやったら、静かに壊れる類の事故だ。

だから設計を変えた。テキスト欄を値の正本として残し、カレンダーは横に添えるだけにした。 カレンダーは入力補助であって、値の解釈には関わらない。ゼロ日付の行を開くと、テキスト欄には 0000-00-00 が残ったまま、カレンダーだけが空で開く。日付を選んだ時点で初めて値が入る。

数えなければ、素直に <input type="date"> にしていた。先に数えたから、置き換えないという判断ができた。

ついでに分かったことも書いておく。次期バージョンで使う新しい MySQL では、既定の設定だとこの値はそもそも入らない。エラーで弾かれる。今は設定を明示的に緩めて通している。動いてはいるが、宿題であることに変わりはない。データ側とプログラム側の両方に判定が散らばっているので、片方だけ直すとかえって中途半端になる。まとめて考える日を別に取ることにした。

ひな型ボタンは、意外と使っていた

phpMyAdmin に、テーブルを開くと下に並ぶボタンがある。SELECT * SELECT INSERT UPDATE DELETE。押すとSQLの雛形が窓に入る。

自分では使っていないつもりだった。振り返ると、意外と使っていた。 列が20本ある表の INSERT を手で打ちたい人はいない。あれは「列名を手打ちしないための道具」だ。

同じものを足した。ただし、そのままは真似していない。

値は [列名] というプレースホルダにした。 直さずに実行すると MySQL が構文エラーで止まる。うっかり流しても何も起きない。実際に流して1064で止まることを確かめた。

UPDATE と DELETE の WHERE は、必ず主キーで作る。 全件に効く雛形は出さない。主キーが無いテーブルは WHERE 1 = 0 にして、条件を書かせる。

DELETE FROM (テーブル)
-- ★主キーが無いテーブル。対象を絞る条件を必ず書くこと
WHERE 1 = 0;

この注意書きは、最初は WHERE 1 = 0 と同じ行の末尾に置いていた。動かしてみたら、文末の ;-- コメントに飲まれて文が終わらなくなっていた。コメントを前の行に出して直した。こういうものは書いてみないと分からない。

一列に、カンマ区切りで入れる

MySQL に FIND_IN_SET という関数がある。'3,7,12' のような文字列の中に、ある値が含まれているかを返す。

これを知ってから、設計の引き出しが一つ増えた。一件に複数の区分がぶら下がるデータを、別表にせず1列にカンマ区切りで持つ。

たとえば利用者ごとに「使えるアプリ」の集合を持たせる。

SELECT * FROM アプリ
WHERE FIND_IN_SET(アプリID, '3,7,12')

素直にやるなら「利用者とアプリの対応表」を別に作り、JOIN する。正規化の教科書どおりだ。だがこの用途では、表が1つ増え、JOINが1本増える。 それだけの重さがある。

正直に書いておくと、これは教科書から外れたやり方だ。

  • 索引が効かない。 列の全行を舐めることになる
  • 数が増えれば破綻する。数十、数百になったら別表にすべきだ
  • 参照整合性が効かない。存在しないIDが混ざっても DB は止めてくれない

だから使う場所を選ぶ。数が数個で収まり、その列だけで検索の主戦場を張らない場所。 条件が合うなら、表とJOINを1本減らせる効果の方が大きい。

見つけたときは、正直かなり嬉しかった。手持ちの道具が一つ増えると、設計の選択肢そのものが増える。 前なら「表を増やすしかない」と思っていた場面に、別の答えが立つ。

行の追加は、造らなかった

phpMyAdmin には行を新規登録するフォームがある。今回、これは造らなかった。

理由は単純で、phpMyAdmin でも一度も使ったことがないからだ。 使ったことのない機能を移植しても、使わない。

考えてみると、理屈もある。NGW のデータは、1行だけ手で足しても意味を成さない場面が多い。決裁なら経路の行が、文書なら簿冊との関連が、別の表に要る。トリガもストアドも無い設計だから、手で入れた1行は関連が欠けたまま孤立する。

行を「作る」のはアプリの仕事だ。この道具の役目は、見ることと、直すことである。線を引いたら、造らない理由の方がはっきりした。

使わない機能は、最初から存在しない。前回書いた流儀は変わっていない。引き算で始めて、要るものだけ足す。 一ヶ月やってみて、この順序で正しかったと思う。最初から全部を並べていたら、行追加のフォームも当然のように入っていたはずだ。

AIとの協働作業での学び

今回いちばん効いたのは、数えてから決めたことだった。ゼロ日付が974万件ある、と分かった時点で設計が決まった。分からないまま進めていたら、標準の部品を素直に使って、静かに壊していた。AI は「調べますか」とは言ってくれない。何を数えるべきかを決めるのは、こちら側の仕事だ。

もうひとつは、確かめられない場所から降りる判断である。ネイティブのカレンダーは、私の手元では再現しない。再現しないものを二回直して、二回とも駄目だった。三回目をやる前に、自前で描く方に切り替えた。

当てずっぽうで直し続けるのは筋が悪いので、描画も開閉も自前で持ちます。

AI に何度も直させることはできる。だが検証できない土俵に立ち続ける限り、直ったかどうかも分からない。 土俵を移せば確かめられる。移すという判断だけは、人間がやるしかなかった。

道具は、造ってからの方が長い。一ヶ月で phpMyAdmin を開かなくなった。それは機能が増えたからではなく、自分の手の形に合ってきたからだと思う。

付録 — 現在の機能一覧

一ヶ月経った時点の全機能。単一ファイルの PHP、外部依存ゼロ、ダーク基調という方針は変えていない。

見る

  • DB切替(対象のデータベースだけを一覧に出す)
  • テーブル一覧(TABLE と VIEW を分ける/内部テーブルは隠す/概算件数つき)
  • データ表示(ページ送り)
  • 構造表示(列定義とインデックス)
  • 列のドラッグ&ドロップ並べ替え、表の外へ放り出して非表示、チェックリストで一括切替

直す

  • 行クリックで編集(全列を並べたモーダル。値を直して保存)
  • 行削除
  • 主キーによる行特定、書く直前の1件確認、キー列の固定、変更列だけの更新
  • 日付・日時欄の自前カレンダー(日時は時刻を保ったまま日付だけ差し替え)
  • NULL と空文字の区別(チェックボックスで切替)

流す

  • SQL実行(複数文を ; 区切りで順次実行)
  • SQLひな型(SELECT / INSERT / UPDATE / DELETE。値はプレースホルダ、WHEREは主キー)
  • クイック実行(SELECT * / COUNT(*)
  • 列名パネル(ダブルクリックで挿入、前カンマ自動)
  • SQLファイルの読み込み(ボタン/ドラッグ&ドロップ。.gz も可。窓に入れるだけで実行はしない)
  • EXPLAIN の結果を日本語で簡易分析
  • JOINビルダー(テーブルとキーを選ぶと JOIN 文が組み上がる。同名列は候補提案、数件で噛み合いを確認)

出し入れ

  • 結果を CSV で書き出し
  • SQLダンプの生成(構造のみ/データのみ/両方。画面表示とダウンロード)
  • SQLファイルのインポート(.sql / .sql.gz

守る

  • パスワードによるログイン、接続元IPの制限
  • SELECT 以外を流すときの確認ダイアログ
  • DROP / TRUNCATE はテーブル名入りの強い確認
  • CSRF トークン
  • 主キーが無いテーブルは行編集させない(理由を画面に出す)
  • ひな型は実行しない。値を直すまで構文エラーで止まる

見ると、直すと、流す。それだけの道具だ。それで足りている。