たわごと

PHPでSQLite3、(やっと)はじめました

レンタルサーバーの構成が一新しました。

が、新しいPHPのバージョンではSQLite2が使えなくなってしまい、掲示板システムが運用できなくなってしまいました。一番古いバージョンを選べば運用できたことはできたのですが、これからのことを考えてSQLite3に移行しようとなったわけです。

具体的にはSQLite2を使いやすくするために自前のPHPモジュールを使っていたので、モジュール内の関数名をチャチャッと書き換えたわけですが、ここでPHPにおけるSQLite2とSQLite3の、あまり知られていないような気がする相違点にぶち当たりました。

前半は適当に説明しますが・・・SQLite3でもクエリを実行するためのシンプルな方法はこんな感じですよね。

  1. new SQLite3()でデータベースを開く
  2. SQLite3::query()でクエリを実行する
  3. 帰ってきた結果があればSQLite3Result::fetchArray()で結果を取り出す

SQLite2でもメソッド名が違うだけで、流れとしてはほとんど同じで返り値の扱いが若干変わっているところがあったような気がする程度でした。

が。

結果の行数を知る方法が無くなってて、つまづいた

SQLite2ではSQLiteResult::numRows()にあたるメソッドがSQLite3には無いんですよ。ちょっと調べてもSQLite3Result::fetchArray()を使った後で配列を数える方法しかありませんでした。自分は結果の行数があるかないかで結果を取り出すかどうかを判断していたので、金庫の中にカギを入れてしまった状態になってしまいました。

なんとかSQLite3Result::fetchArray()の前に結果の行数を知るためにSQLite3::query()が返すリソースをcount()してみたりもしましたがダメでした。

正しい移行の方法の一つとして、この場合は結果の行数ではなくSQLite3Result::numColumns()カラムの数がゼロかどうかだけでも調べれば良かったようです。結果が無ければカラムはゼロですもんね。

ちなみにカラムの数で判断せずにSQLite3Result::fetchArray()を強行すると次の悲劇が訪れます。

結果を返さないクエリだとfetchArray()が勝手にクエリ再実行して…詰んだ

カラムを調べることに気づくまでは、結果の行数が得られないままSQLite3Result::fetchArray()を実行してから取り出した配列の数をチェックすることにしました。掲示板システムも動くようになったようです。

しかし、テンポラリテーブルを作るあたりで、なぜか「そのテーブルはすでに作られている」エラーが必ず発生していました。スクリプトを見直してもテンポラリテーブルを作り直している部分は無いし、それに同じスクリプトなのにSQLite2の時にはそんなエラーは出ていませんでした。

公式サイトでもテンポラリテーブルを作成する書式もきちんと定義されているので、テンポラリテーブルが使えなくなったなんて疑問もおかしくなり、何よりテンポラリテーブルを使ったクエリの結果がちゃんと返されているようなのです。とりあえず掲示板は表示に問題なく動いていることだし、そのまま様子を見ながら調査をすることにしていました。

ところが。掲示板に投稿してみたところ、1つの記事を投稿したはずなのに全く同じ記事が2つ投稿してしまいました。これは致命的です。もちろん2重投稿禁止の機能も付いていますので、操作ミスで2重投稿した可能性は低いですし、実際に検証もしました。

何かヒントがないか藁をもすがる思いでPHPマニュアルサイトを眺めていたら、SQLite3Result::fetchArray()の説明ページの下のほう、User Contributed Notes (ユーザー投稿ノート、もちろん全部英語)に次のようなこと(たぶん)が書かれていました。

マニュアルの内容とは異なり、SQLite3::query()は、結果を返すクエリ(SELECTEXPLAIN)だけでなく常にSQLite3Resultインスタンスを返します。SQLite3Result::fetchArray()が「結果が返らない」クエリの結果に対して呼び出されるたびに、内部的にクエリが再度実行され、おそらくアプリケーションが失敗するでしょう。

なんということでしょう!。自分のつまづいていた原因がpaule-pankeさんによって報告されていました。さらに次のようなアドバイスも(たぶん)書かれています。

SQLite3Result::fetchArray()を呼び出す前に、SQLite3Result::numColumns()で空の結果かどうかを確認してください。
クエリが行を返すかどうかを知ることはできません。ただ、SQLite3::query()SELECTクエリを実行しなければ解決する、というわけではありません。(訳注:後半の翻訳に自信ない)

結果としてはこの挙動そのものを治す手段はありませんが、前述のSQLite3Result::numColumns()でカラム数を前もって調べておき、結果がある場合のみSQLite3Result::fetchArray()を実行することで解決できました。

理想としてはSQLite3::query()がマニュアル通りに結果が無ければfalseを返してくれればいいんですけどね。SQLite3::exec()があるのは知ってるけど、SQLite3::query()で「結果が返らない」クエリを実行させたい場合もあるんですよ。今回のつまづきは、SQLite2の時は大丈夫だったのに、SQLite3::query()CREATE TEMPORARY TABLEのようなクエリを発行したのがきっかけでした。

エラー無いのにエラーメッセージが返ってきて、ズッコケた

とりあえず掲示板のメンテナンスツールで確認してみたところ、結果が返るクエリについては問題ないものの、コメント行「--」(ハイフン2つ)でエラーになってしまいました。コメント行も「結果が返らない」クエリであり、ここでエラーになってしまうと掲示板でもエラーになってしまいます。

ただ、これまでのドタバタ騒ぎ?で心当たりがあったので、そこを重点的にチェックしてみます。SQLite2でエラーメッセージを取得するにはsqlite_query()の実行時に参照渡ししておいた変数に格納されるのに対し、SQLite3ではSQLite3::lastErrorMsg()で取得するようになっていました。

しかし、このメソッドはエラーが無くてもnot an errorと律儀に返すヤツで、この内容が空だったらエラー有りだと判断するようにしてしまうと、とんでもないことになってしまいますね。

したがって、SQLite3::lastErrorCode()がゼロならエラー無し、ゼロ以外ならSQLite3::lastErrorMsg()を使うようにします。


トップページへ

こな