2008.06.16 Monday
PHP と DB と NUMERIC 〜暗黙の型変換の罠〜
この現象は Sybase 限定なのかもしれませんが, 有害な PHP の情報共有も兼ねて。
携帯以外からのアクセス制限しているとはいえ PHP 4.3.x という化石のような環境を何とかすべく, 4.3.x → 4.4.8 へアップデートしました。
# 4系はサポート終わっちゃってますけどいきなり 5系は厳しそうだったので...
アップデート後は特に問題なく平和に動いていたサイトですが, 数日後から障害っぽいメールがちらほらと。
とあるゲームのコーナーなんですが, 条件を満たしていないのに次に進める模様。
詳しく調査してみると,
2 > 16
が true と判定(???)されていることが判明。
Ω ΩΩ< 「な、なんだってー!!」
携帯以外からのアクセス制限しているとはいえ PHP 4.3.x という化石のような環境を何とかすべく, 4.3.x → 4.4.8 へアップデートしました。
# 4系はサポート終わっちゃってますけどいきなり 5系は厳しそうだったので...
アップデート後は特に問題なく平和に動いていたサイトですが, 数日後から障害っぽいメールがちらほらと。
とあるゲームのコーナーなんですが, 条件を満たしていないのに次に進める模様。
詳しく調査してみると,
2 > 16
が true と判定(???)されていることが判明。
Ω ΩΩ< 「な、なんだってー!!」
該当の不等式は DB から引っ張ってきた値同士を比較しているのですが, そのカラムの型が NUMERIC になっていました。
NUMERIC は“正確な”数値データ型で, 全体の桁数と小数点の有効桁数を指定して定義できるようになっています。
NUMERIC(precision, scale)
precision: 数字全体の有効桁
scale: 小数点の右側の小数点以下の桁数
小数部の精度を維持するために, DB には数値としてではなく文字列やバイナリ等で格納されるようです。
さて, この NUMERIC型のカラムを PHP で取得するとどうなるか。
取得した値を var_dump してみると...
・バージョンアップ前 (PHP 4.3.1〜4.3.4)
2 → string(3) "2.0"
16 → string(4) "16.0"
・バージョンアップ後 (PHP 4.4.8)
2 → string(28) "2.0 "
16 → string(28) "16.0 "
実装が変わったせいなのか, バージョンアップ後は string(28) 固定になっているじゃないですか!
後ろにスペースが入った場合の不等式は以下の様になりました。
var_dump("2.0" > "16.0") → false
var_dump("2.0 " > "16.0 ") → true(!)
PHP の“余計な”暗黙の型変換により "2.0" と "16.0" という文字列は数値に変換されて比較されるので正しく動いていましたが,
後ろにスペースが入ると数値に変換されず, 文字列のままで比較されて "2.0 " > "16.0 " という式が成立してしまっていたのです。
バージョンアップでの NUMERIC の取得形式の変更と PHP の暗黙の型変換との併せ技で見事にハマってしまいました...こんなことが起こるのは PHP だけでしょう。
PHP での不等式の比較の際は徹底的に型指定した方が事故らなくていいでしょうね。
PHP の暗黙の型変換はカオス過ぎて, どこで文字列でどこで数値になってるかわかんないですし。
いい加減, PHP には愛想が尽きてきました(´・ω・`)
というか, 実は今回の件ではそもそも NUMERIC型を使う必要なんてコレっぽっちもなかったんですけどね...はぁ。
[MySQL: NUMERIC]
MySQL 4.1 リファレンスマニュアル :: 6.2.1 数値型
MySQL 5.1 リファレンスマニュアル :: 10.2 数値タイプ
値の小数部の精度を維持するため文字列で格納。
MySQL 5.0.3バージョン以前では, 文字列として格納されていた模様。
[PostgreSQL: NUMERIC]
PostgreSQL 8.3.1文書 8.1数値データ型 任意の精度を持つ数
[参考]
EC studio: PHPの文字列比較で気をつけるべきこと - 暗黙の型変換
NUMERIC は“正確な”数値データ型で, 全体の桁数と小数点の有効桁数を指定して定義できるようになっています。
NUMERIC(precision, scale)
precision: 数字全体の有効桁
scale: 小数点の右側の小数点以下の桁数
小数部の精度を維持するために, DB には数値としてではなく文字列やバイナリ等で格納されるようです。
さて, この NUMERIC型のカラムを PHP で取得するとどうなるか。
取得した値を var_dump してみると...
・バージョンアップ前 (PHP 4.3.1〜4.3.4)
2 → string(3) "2.0"
16 → string(4) "16.0"
・バージョンアップ後 (PHP 4.4.8)
2 → string(28) "2.0 "
16 → string(28) "16.0 "
実装が変わったせいなのか, バージョンアップ後は string(28) 固定になっているじゃないですか!
後ろにスペースが入った場合の不等式は以下の様になりました。
var_dump("2.0" > "16.0") → false
var_dump("2.0 " > "16.0 ") → true(!)
PHP の“余計な”暗黙の型変換により "2.0" と "16.0" という文字列は数値に変換されて比較されるので正しく動いていましたが,
後ろにスペースが入ると数値に変換されず, 文字列のままで比較されて "2.0 " > "16.0 " という式が成立してしまっていたのです。
バージョンアップでの NUMERIC の取得形式の変更と PHP の暗黙の型変換との併せ技で見事にハマってしまいました...こんなことが起こるのは PHP だけでしょう。
PHP での不等式の比較の際は徹底的に型指定した方が事故らなくていいでしょうね。
PHP の暗黙の型変換はカオス過ぎて, どこで文字列でどこで数値になってるかわかんないですし。
いい加減, PHP には愛想が尽きてきました(´・ω・`)
というか, 実は今回の件ではそもそも NUMERIC型を使う必要なんてコレっぽっちもなかったんですけどね...はぁ。
[MySQL: NUMERIC]
MySQL 4.1 リファレンスマニュアル :: 6.2.1 数値型
MySQL 5.1 リファレンスマニュアル :: 10.2 数値タイプ
値の小数部の精度を維持するため文字列で格納。
MySQL 5.0.3バージョン以前では, 文字列として格納されていた模様。
[PostgreSQL: NUMERIC]
PostgreSQL 8.3.1文書 8.1数値データ型 任意の精度を持つ数
[参考]
EC studio: PHPの文字列比較で気をつけるべきこと - 暗黙の型変換









Comments
テスト書くのはやっぱり面倒ですが, 安心感はありますね。
欠かせないとまではまだ感じませんが, サービス中のプログラムのリファクタリングには前向きになれることは間違いないでしょう。
逆に S-in 前は TDD を徹底しなくてもいい気もします。
ファイル読み込み, DBアクセスが絡むメソッドのテストなんかも面倒くさいですね。
モック書くのも面倒だし, 何かいい方法あるんですかね。
PHPいい加減とは言っても、それ自身が利点でもあるわけで、諸刃の剣という感じでしょうか。
TDDは、ハマるとヤメられなくなると言いますが、実際どうなんでしょうね。ヤメられなくなった人の話を聞いてみたいなぁ。インタフェースに関する部分は全部テストせぃと言われても、「そ、そんな、ご無体な、、、」と思ってしまいます。テスト生成の自動化ができれば良いんでしょうが、やれてもせいぜい補助的にしかできなさそうですし。
DocTestか。確かに一つのアプローチなんでしょうね。ある意味3次元的なプログラミングですね。
渋谷で飲んでからもう数年ですか...早いもんですね。
TDD ですか...実践していれば間違いなくこういった事故は減りますよね。
ただ, 今回の件はきちんと型を意識してテストケースを書いていなければテストは通ってしまいますけどね...PHP いい加減すぎるんで。
障害を回避できるかどうかはテストの質によるので, テストを書く人次第だとは思いますが。
ちなみに私はテストは確かに重要だと思いながらも, 面倒くさくて TDD は実践してません。
ユニットテストを実践していた時期はあったのですが, 実装のテンポが悪すぎて...。
後, ユニットテストの環境を用意するのも面倒なんですよね。
なので DocTest は結構いい感じなんじゃないかと思います。
使い勝手の良い DocTest環境があれば導入してみたいですね。
ロジックがテスト用のコメントの嵐になってしまうので, エディタが対応してくれなければ厳しいですが...。
何年か前の渋谷以来でしょうか。
(このヒントだけで事足りなければ、突っ込んで下さい (^^;)
個人事業始めたと書いてあって、ちょっとびっくりしました。
それは置いておいて、スクリプト系言語だと、どうしてもこういう問題に遭遇しますよね。
agile な考え方によると、Test Driven Development によってこういう問題は回避すべきだなんて言われているような気がするのですが、
・それは僕の誤解でしょうか?
・本当に回避できるもんでしょうか?
・TDD 実践してますか?
僕は、そんなこんなで、やっぱり Java から離れられません。。。