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 トークン
- 主キーが無いテーブルは行編集させない(理由を画面に出す)
- ひな型は実行しない。値を直すまで構文エラーで止まる
見ると、直すと、流す。それだけの道具だ。それで足りている。