2016/10/18 20:25:07

PDOでリザルトを取得するときのモードをいろいろ試してみた

php-logo
目次(クリックするとジャンプします)
  • 1:データベースとトモダチになる
  • 2:ちょっとした説明
  • 3:取得モードいろいろ
  • 3.1:FETCH_BOTH 連想配列と添字配列
  • 4:FETCH_ASSOC 連想配列のみ
  • 4.1:FETCH_NUM 添字配列のみ
  • 4.2:FETCH_COLUMN 指定したコラム番号に対応した値を取得
  • 4.3:FETCH_CLASS 指定したクラスのオブジェクトとして取得
  • 4.4:FETCH_COLUMN 既存オブジェクトの更新として取得
  • 4.5:FETCH_OBJ 匿名オブジェクトとして取得
  • 4.6:FETCH_LAZY PDORowオブジェクトとして取得
  • 5:補足・ハマったポイント
  • 5.1:オブジェクトで結果を受け取る際のプロパティのアクセス権限
  • 5.2:初期化処理
  • 6:まとめ

データベースとトモダチになる

PHPでデータベースとトモダチになるならPDOを使うのが一般的のようです。@MINOも大分データベースと仲良くなってきましたが、まだまだ奥が深く、いつ仲違いしてしまうか油断は禁物です。

今回はやはりデータベースのデータをしっかり扱いたいという欲求からPDOqueryメソッドのリザルトの取得モードについてまとめてみました。

単発のSQLを実行するならqueryメソッドは楽ですよね。

ちょっとした説明

queryメソッドの戻り値はPDOStatementです。

@MINOなんかはforeachで回して配列に収めた後に処理していましたが、これは無駄でした。

いちいち配列などに格納する必要はありません。

PDOStatementオブジェクトはトラバーサル(横断的)にアクセス可能なので、そのままforeachで回せます。

$sql = 'SELECT * FROM testtable';
foreach ($pdo->query($sql, PDO::FETCH_BOTH) as $value) {
    var_dump($value);
}

取得モードいろいろ

PDOでは実に多彩に結果の取得形式を指定することが可能です。

以下のようなデータベーステーブルでテストしてみました。

id name age
1 saitou 23
2 sasayama 39
3 ikariya 33
4 miyama 23
5 oota 39

まず全体的なコードです

ここから一個ずつ説明していきたいと思います。

FETCH_BOTH 連想配列と添字配列

デフォルトの取得モードです。連想配列を添字配列の両方が一度に出力されます。

echo "-デフォルト--------------------------------\n\n";

//デフォルト
//連想配列と添字配列のハイブリット
foreach ($pdo->query($sql, PDO::FETCH_BOTH) as $value) {
    var_dump($value);
}

一度で二度美味しいのですが、どちらかだけが必要になる場合が多い様に思えるので、意外と美味しくない取得方法かもしれません。

-デフォルト--------------------------------

array(6) {
  ["id"]=>
  string(1) "1"
  [0]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  [1]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
  [2]=>
  string(2) "23"
}
array(6) {
  ["id"]=>
  string(1) "2"
  [0]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  [1]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
  [2]=>
  string(2) "39"
}
array(6) {
  ["id"]=>
  string(1) "3"
  [0]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  [1]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
  [2]=>
  string(2) "33"
}
array(6) {
  ["id"]=>
  string(1) "4"
  [0]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  [1]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
  [2]=>
  string(2) "23"
}
array(6) {
  ["id"]=>
  string(1) "5"
  [0]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  [1]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
  [2]=>
  string(2) "39"
}

また必然的にデータが倍になるので、大きい結果だとメモリを圧迫するかもしれません。

なんにも考えなくても配列として回せるというのは便利だとは思います。

ネット上ではこのデフォルトは嫌われているみたいです。

FETCH_ASSOC 連想配列のみ

こちらは連想配列のみです。場合によってはデフォルトよりこっちの方が使いやすいかもしれません。

echo "\n-連想配列--------------------------------\n\n";

//連想配列
//コラム名がプロパティ名になる
foreach ($pdo->query($sql, PDO::FETCH_ASSOC) as $value) {
    var_dump($value);
}

デフォルトよりデータは2分の1になるのでメモリの圧迫も軽減できるかもしれません。

-連想配列--------------------------------

array(3) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
}
array(3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
array(3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
array(3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
array(3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
}

FETCH_NUM 添字配列のみ

こちらは添字配列のみの取得になります。PHPは言語仕様上、添字配列と連想配列は同じものなので、扱いは連想配列同じにできます。

echo "\n-添字配列--------------------------------\n\n";

//添字配列
//0から始まる番号がプロパティ名となる
foreach ($pdo->query($sql, PDO::FETCH_NUM) as $value) {
    var_dump($value);
}

カウントで何らかの処理を回したい場合は添字配列の方が便利な場合もあります。

-添字配列--------------------------------

array(3) {
  [0]=>
  string(1) "1"
  [1]=>
  string(6) "saitou"
  [2]=>
  string(2) "23"
}
array(3) {
  [0]=>
  string(1) "2"
  [1]=>
  string(8) "sasayama"
  [2]=>
  string(2) "39"
}
array(3) {
  [0]=>
  string(1) "3"
  [1]=>
  string(7) "ikariya"
  [2]=>
  string(2) "33"
}
array(3) {
  [0]=>
  string(1) "4"
  [1]=>
  string(6) "miyama"
  [2]=>
  string(2) "23"
}
array(3) {
  [0]=>
  string(1) "5"
  [1]=>
  string(4) "oota"
  [2]=>
  string(2) "39"
}

PHPの添字配列は処理の過程において歯抜けになる可能性があるので、カウントで回す時は十分気を付ける必要があります。

FETCH_COLUMN 指定したコラム番号に対応した値を取得

指定したコラム番号の値のみを取得します。値を一部だけ取得したい場合や、なんらかのデータ構造に組み替える場合には便利かもしれません。

echo "\n-コラム番号指定--------------------------------\n\n";

//コラム番号指定
//指定した番号に対応するコラムの値が取得される
foreach ($pdo->query($sql, PDO::FETCH_COLUMN, 1) as $value) {
    var_dump($value);
}

一番最初のコラムは0から始まります。

-コラム番号指定--------------------------------

string(6) "saitou"
string(8) "sasayama"
string(7) "ikariya"
string(6) "miyama"
string(4) "oota"

FETCH_CLASS 指定したクラスのオブジェクトとして取得

おそらく一番汎用性が高い取得方法だと思います。データベースの行をクラスとして扱えますし、独自のメソッドを搭載できるのでデータ操作をカプセル化できます。

echo "\n-クラス指定--------------------------------\n\n";

//クラス指定
//指定したクラスにカラム名がプロパティ名として値が取得される
foreach ($pdo->query($sql, PDO::FETCH_CLASS, DBData) as $value) {
    var_dump($value);
}

指定するクラスは自作のクラスでもOKですし、PHPの標準クラスであるstdClassを使ってもOKです。

コラム名がプロパティ名になり、対応した値が入ります。

-クラス指定--------------------------------

object(DBData)#4 (3) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
object(DBData)#4 (3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
object(DBData)#4 (3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
}

FETCH_COLUMN 既存オブジェクトの更新として取得

こちらは既存のオブジェクトのプロパティを更新する形で結果を取得します。

echo "\n-オブジェクト指定--------------------------------\n\n";

//オブジェクト指定
//指定した既存のオブジェクトのカラム名に対応するプロパティを更新
$dbData = new DBData();//stdClassでもOK 
foreach ($pdo->query($sql, PDO::FETCH_INTO, $dbData) as $value) {
    var_dump($value);
}

データ更新に近しいので、その特性を活かした処理では重要なモードになると思います。

-オブジェクト指定--------------------------------

object(DBData)#2 (3) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
}
object(DBData)#2 (3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
object(DBData)#2 (3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
object(DBData)#2 (3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
object(DBData)#2 (3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
}

FETCH_OBJ 匿名オブジェクトとして取得

匿名オブジェクトとして結果を取得する方法です。

基本的には「指定したクラスのオブジェクトとして取得」でstdClassを用いた際となんら違いは無いと思います。

echo "\n-匿名オブジェクト--------------------------------\n\n";

//オブジェクト指定
//カラム名のプロパティが入った匿名オブジェクトとして取得
foreach ($pdo->query($sql, PDO::FETCH_OBJ) as $value) {
    var_dump($value);
}

@MINOには違いがよくわかりません。

-匿名オブジェクト--------------------------------

object(stdClass)#4 (3) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
}
object(stdClass)#6 (3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
object(stdClass)#4 (3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
object(stdClass)#6 (3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
object(stdClass)#4 (3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
}

FETCH_LAZY PDORowオブジェクトとして取得

PHPマニュアルには

PDO::FETCH_LAZY: PDO::FETCH_BOTH とPDO::FETCH_OBJの 組合せで、オブジェクト変数名を作成します。

PDOStatement::fetch
http://php.net/manual/ja/pdostatement.fetch.php

と説明がありますが、どういったことを意図しているモードなのか把握できませんでした。

echo "\n-PDORowオブジェクト--------------------------------\n\n";

//PDORowオブジェクト
//PDORowオブジェクトとして取得
foreach ($pdo->query($sql, PDO::FETCH_LAZY) as $value) {
    var_dump($value);
}

実際に使ってみるとPDORowというクラスでの取得になり、SQLが格納されている点が他のオブジェクトとは違う点です。

-PDORowオブジェクト--------------------------------

object(PDORow)#6 (4) {
  ["queryString"]=>
  string(23) "SELECT * FROM testtable"
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(6) "saitou"
  ["age"]=>
  string(2) "23"
}
object(PDORow)#6 (4) {
  ["queryString"]=>
  string(23) "SELECT * FROM testtable"
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
object(PDORow)#6 (4) {
  ["queryString"]=>
  string(23) "SELECT * FROM testtable"
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
object(PDORow)#6 (4) {
  ["queryString"]=>
  string(23) "SELECT * FROM testtable"
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
object(PDORow)#6 (4) {
  ["queryString"]=>
  string(23) "SELECT * FROM testtable"
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"
}

ネットをいろいろ検索してみましたが、クリティカルな情報を得ることができませんでした。

いくつかのページではセキュリティ情報に関係がありそうな点も散見しましたが、よくわかりません。

使う場面があるんだろうか?

補足・ハマったポイント

オブジェクトで結果を受け取る際のプロパティのアクセス権限

今回例に示したクラスは以下の様にプロパティを何も定義していません。

//結果取得用クラス
class DBData {
    public function __get($name)
    {
        return $this->$name;
    }
}

このクラスのオブジェクトを使ってFETCH_INTO等で結果を取得する場合、コラム名に対応するプロパティがなければ、コラム名のプロパティ名となったプロパティが自動的に作られます。

しかし事前にコラム名と同じプロパティを作っておけばそこに値が入るようになります。

//結果取得用クラス
class DBData {

    public $id;
    public $name;
    public $age;
 
    public function __get($name)
    {
        return $this->$name;
    }
}

この場合、プロパティのアクセス権をpublic以外にしていた場合、FETCH_INTO等のオブジェクトの更新による取得はエラーがでて止まります。

$idをprivateにした場合

Fatal error:  Uncaught Error: Cannot access private property DBData::$id in /var/www/html/pdo.php:27

$idをprotectedにした場合

Fatal error:  Uncaught Error: Cannot access protected property DBData::$id in /var/www/html/pdo.php:27

プロパティが存在しない場合は基底クラスのマジックメソッドの__setでプロパティを作っているのではないかと思います。

もしコラム名と対応するプロパティがある場合は__setは呼ばれず直接アクセスしようとしているのだと思います。

プロパティがpublicでしか扱えないのはやや不安ですので、できるだけprivateにしたいですよね。

そこで__setのオーバライドです。

以下の様に__setメソッドをオーバーライドして値に何らかの操作を加えるようにしてみました。

class DBData {
    public function __set($name, $value)
    {
        $this->name = $value."<-set?";
        return;
    }
    public function __get($name)
    {
        return $this->$name;
    }
}

echo "\n-オブジェクト指定--------------------------------\n\n";

//オブジェクト指定
//指定した既存のオブジェクトのカラム名に対応するプロパティを更新
$dbData = new DBData();//stdClassでもOK 
foreach ($pdo->query($sql, PDO::FETCH_INTO, $dbData) as $value) {
    var_dump($value);
}

そうすると以下のように反映されます。

-オブジェクト指定--------------------------------

object(DBData)#3 (3) {
  ["id"]=>
  string(6) "1<-set"
  ["name"]=>
  string(11) "saitou<-set"
  ["age"]=>
  string(7) "23<-set"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "sasayama"
  ["age"]=>
  string(2) "39"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "3"
  ["name"]=>
  string(7) "ikariya"
  ["age"]=>
  string(2) "33"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "4"
  ["name"]=>
  string(6) "miyama"
  ["age"]=>
  string(2) "23"
}
object(DBData)#3 (3) {
  ["id"]=>
  string(1) "5"
  ["name"]=>
  string(4) "oota"
  ["age"]=>
  string(2) "39"

コンテキスト上のクラスの__setが使われているのであれば、プロパティをprivateにしても値を入れることが可能なはずです。

//結果取得用クラス
class DBData {
    private $id;
    private $name;
    private $age;
    public function __set($name, $value)
    {
        $this->$name = $value;
        return;
    }
    public function __get($name)
    {
        return $this->$name;
    }
}

echo "\n-オブジェクト指定--------------------------------\n\n";

//オブジェクト指定
//指定した既存のオブジェクトのカラム名に対応するプロパティを更新
$dbData = new DBData();//stdClassでもOK 
foreach ($pdo->query($sql, PDO::FETCH_INTO, $dbData) as $value) {
    var_dump($value);
}

以下のように出力されました。

object(DBData)#3 (3) {
  ["id":"DBData":private]=>
  string(1) "1"
  ["name":"DBData":private]=>
  string(6) "saitou"
  ["age":"DBData":private]=>
  string(2) "23"
}
object(DBData)#3 (3) {
  ["id":"DBData":private]=>
  string(1) "2"
  ["name":"DBData":private]=>
  string(8) "sasayama"
  ["age":"DBData":private]=>
  string(2) "39"
}
object(DBData)#3 (3) {
  ["id":"DBData":private]=>
  string(1) "3"
  ["name":"DBData":private]=>
  string(7) "ikariya"
  ["age":"DBData":private]=>
  string(2) "33"
}
object(DBData)#3 (3) {
  ["id":"DBData":private]=>
  string(1) "4"
  ["name":"DBData":private]=>
  string(6) "miyama"
  ["age":"DBData":private]=>
  string(2) "23"
}
object(DBData)#3 (3) {
  ["id":"DBData":private]=>
  string(1) "5"
  ["name":"DBData":private]=>
  string(4) "oota"
  ["age":"DBData":private]=>
  string(2) "39"
}

エラーが出ずにちゃんとprivateで値が入っています。これでカプセル化は一応できそうです。

初期化処理

以上の事を踏まえると、__setを工夫することで何らかの初期化的処理が値に対して行えることになります。

値のバリデーションなどを行うには良いのかもしれませんが、__setの本来の役割も潰れる結果になってしまうかもしれません。

この辺のことはもっと勉強しないといけなさそうです…。

まとめ

いろいろな場合があるとは思いますが、おすすめはやっぱりFETCH_CLASSですね。一番拡張性が高い気がします。

行をクラスで扱えるのはやっぱり便利ですよ。

  • PDOのqueryメソッドでは結果の取得モードを設定できる
  • 連想配列、添字配列、クラス、オブジェクト、コラム番号など多彩にモードを変更できる
  • プロパティのセットはマジックメソッドの__setが使われている模様
  • __setメソッドをオーバーライドすることでプロパティをprivateに保つことも可能
  • __setメソッドをオーバーライドすることで値になんらかの初期化的処理を行うことも可能