TOP

還暦過ぎのITはつらいよ!

プログラミング

phpフレームワーク自作8

データベース接続処理

ねぎぼうず

アプリケーションを動かすにあたって、データをどのように扱うかが重要なことであると考えます。
 ウェブアプリケーションの場合は、ユーザからの入力は、クエリー情報として受け取ることになります。また、必要におおじて、サーバ上にあるファイルやデータベースからデータを引き出して使われることになります。
フレームワークでの処理は、クエリー情報の処理手順から得た情報とデータベースからのデータの取り出しを行うことによって、構築していくことになります。(但し、データベースを使わない場合もあります。)
クエリーの処理はリクエスト処理の所でやりましたので、ここではデータベースからのデータの扱いについて考えて生きたいと思います。

目次

RDB リレーショナルデータベース

データベースとはいろいろな情報を、決められたフォーマットで集め、蓄積されたものであるとされております。

 どのようなフォーマットで集めるかは、データモデルとしていろいろ考えられているようです。

そのなかでも、アプリケーション開発で使われるものとして、リレーショナルデータベース(Relational Database RDB)がデータモデルとして古くから採用されてきていました。

その概念については、いろいろと語られておりますが、単純に解釈すると、表を連想するといいでしょう。表の項目が「フィールド」であり、その項目の下にレコードが蓄積されていくイメージだと思います。


<参考サイト>

DTOとDAO

データベース・DTO・DAO連携
データベース・DTO・DAO連携

DAOとは Data Access Objectの略称です。データベースへのアクセスとデータの操作を担います。

DTOとは Data Transfer Objectの略称です。データベースから取得したデータをオブジェクトとして格納したものです。一般的には、プロパティとそれを格納・取得するだけのメソッドのみを実装しています。RDBにおける、フィールドはオブジェクトのプロパティとして、定義され、レコードは個々のオブジェクトとして、取り込まれることになります。オブジェクトのメソッドはプロパティへの格納と、引き出し(いわゆる、セッッターゲッターメソッド)だけの実装としての機能だけを提供しています。

ここでは、DTOを引数にして、DAOのメソッドを操作したり、メソッドの返り値として、DTOを利用しております。


<参考サイト>

データベースの構築

データベースを定義する。

このフレームワークでは、mysqlまたはsqliteというデータベース製品を使うことを想定しております。両者はいずれも無償のオープンソースのデータベースであり、処理能力の速さに定評があります。現在世界で最も使用されている、オープンソースでもあります。レンタルサーバに導入されていることも多く、簡単に使うことが出来ます。但し、mysqlとsqliteでは以下の点に違いがあることを頭に入れておく必要があると思われます。

  • mysqlはサーバーとして動作しますが、sqliteはphpエンジンの標準ライブラリーとして動作するので、phpエンジンが動いていれば、使うことが出来る。
  • sqliteはファイルとして、格納される。
  • 1ファイル、1データベースである。
  • サーバー側ではphpエンジンがあれば、別途、データベースサーバーを用意する必要がない。
  • 動作が軽量であるが、小規模アプリケーション向きであり、中大規模案件にはむかない。

mysqlデータベースの作成方法(ターミナルウィンドウ)

1.mysqlサーバを起動する。(unixの場合。xamppはコントロールパネルより)

# /etc/init.d/mysqld start

2. mysqlがインストールされているフォルダー内のbinフォルダーにはいる。

3. 管理者パスワードを設定する。(xamppはコントロールパネルphpMyAdminからでも設定できる。)

# mysqladmin -u root password 'new-password'

4. mysqlクライアントをうごかす。

# mysql -u root -p
Enter password:

5. 管理者パスワードを入力し、操作プロンプトを表示させる。

mysql>

6. データベースを作成する。

mysql> CREATE DATABASE データベース名

7. mysqlクライアント画面を閉じる。

mysql> quit;

  

sqliteデータベース(データベースファイル)作成方法

1.php情報を出力(phpinfo())し、pdo_sqliteの項目の存在を確認する。バージョン3であることを確認する。xamppの場合は、下記のようにブラウザで表示できる。

サーバーホスト名/dashboard/phpinfo.php

2.ターミナルウィンドウを開き、作成する任意のフォルダー(phpのパスが通っていればどこでも可能です。)に移動する。ここでは、framework3フォルダーと同一の階層内にdataフォルダーを作り、そこに移動する。

C:\xampp\data>

3. データベースファイルを作成する。存在するファイル名を指定した場合はそのファイルに対しての操作となる。

C:\xampp\data>sqlite3 データベースファイル名

4. sqliteコマンド入力表示になる。sql文を定義し、テーブル作成操作も出来るが、ここではphpテーブル作成、初期設置ファイルから、作成することにしているので、一旦終了させる。

sqlite> .exit;
5. テーブル初期生成phpファイルを作成し、ブラウザにて生成させる。

news3初期テーブル生成コード


<参考サイト>

データベースの接続

phpからデータベースに接続しデータをやり取りするには、データベース固有のPHP Data Objects (PDO) ドライバーとPDO拡張モジュールを使うことが、一般的になっております。これにより、データベースの差異を吸収し、pdoで用意されている、関数を使ってデータを操作できることになります。

phpフレームワーク内でデータベースに接続するには、このpdoのインスタンスを生成させて利用することになります。コンストラクタは以下のようになっております。

public PDO::__construct ( string $dsn [, string $username [, string $password [, array $options ]]] )

sqliteではデータベースファイルで管理されているので、ユーザ・パスワードという定義はありません。sqliteとmysqlの生成構文は以下のようになります。

sqliteの場合
  $pdo = new PDO('sqlite:'. $dbfile);
mysqlの場合
  $dsn = 'mysql:dbname='.DB_NAME.';host='.DB_HOST.';charset=utf8';
  $pdo = new PDO($dsn,DB_USER,DB_PASS);

フレームワークでのデータベース接続

フレームワークにおけるデータベース処理の流れ
フレームワークにおけるデータベース処理の流れ

class DB
{
  const CONFIG_PATH = データベース接続情報を返すファイルパス
  private $dbs = [];  // 使用するデータベースハンドラー
  private $conf = [];  // 接続情報格納用
  private function __construct()
  {
    // 接続情報の取得.
    $this->conf = require self::CONFIG_PATH;
  }
  private function connect($dsn)
  {
    // pdoオブジェクト生成。
    $this->dbs[$dsn] = new \PDO($conf['dsn'], $conf['user'], $conf['password'], $options);
  }

  public static function getDB($dsn)
  {
    $instance = new static;
    $instance->connect($dsn);
	
    // pdoオブジェクトを返す。
    return $instance->dbs[$dsn];
  }
}

-----------------------------

dbinit.php データベース接続情報を返すファイル
  $local_dbinfo = 使用データベースにおけるpdoコンストラクタ生成時の引数を文字列にしたもの。
	* 上記「データベース接続」参照
  $remote_dbinfo =
	
  return [
	  'local' => ['dsn' => $local_dbinfo],
	  'remote' => ['dsn' => $remote_dbinfo]
	 ]

-------------------

config.php アプリケーションで使われるグローバルな設定を定義するファイル

// 使用するdsn名ぶん登録DSN1, DSN2, DSN3,........
if (!defined('DSN1')) {
    define('DSN1', 'local'); # dbinit.phpのreturn 連想配列のkey名に対応.
}

// dbはsqliteであるか否か
if (!defined('LITE')) { # SQLITEを使う場合は1にする。その他は0
    define('LITE', 1);
}

データベースの接続処理はDBクラスで行っております。ここでは接続情報を返すdbinit.phpを取り込むことによって、データベース接続を実現しております。

dbinit.phpにデータベース情報を定義すれば、複数のデータベースハンドラーを取り込むことが出来ます。

また、config.phpに接続情報を定数として定義すれば、定数定義を入れ替えるだけで、開発時と本番環境時の切り替えることができるようになっております。

尚、ソースコードの詳細は下記に示しております。

データの処理方法

SQLは、リレーショナル・データベースにアクセスするためのデータベース言語です。SQLを使って、テーブルの作成・削除や、データの検索・更新・削除といった作業ができます。フレームワークでは、pdoオブジェクトを使って、SQL文を記述し、データベースを操作することが出来ます。

ここでは、既存のフレームワークで取り入れられているような、データベースから引き出されたデータをラッピングせず、そのまま利用する方針でいきたいと考えております。「sql文でできることは、sql文にまかせる。」ということになります。sql文において、かなり複雑な処理を実現することが出来るという認識をもっておりますので、わざわざphpフレームワークのコアの部分で、オブジェクト化しなくてもいいのではないかと考えております。このことについては、賛否両論ありますが、小規模なアプリケーション製作を念頭においておりますので、このような方向性で作り上げていくことになりました。

sql文での処理

DBクラス、Daoクラス、Dao継承クラス内のメソッドがこの処理を担っております。以下、sql文を使ってのメソッドの定義を示しております。


DBクラス

    /*
     * トランザクション実行.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     */
    public static function transaction($dsn)

	
    /*
     * コミット.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2.......
     */
    public static function commit($dsn)	
	
	
    /*
     * ロールバック.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     */
    public static function rollback($dsn)


    /*
     * プリペアドステートメントprepareを使わない、クエリー実行、結果取得.
     * @param $sql sql文
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return PDOStatement
     */
    public static function query(string $sql, string $dsn): \PDOStatement


    /*
     * プリペアドステートメントprepareを使わない、データ取得を伴わないselect,insert,delete,updateクエリー実行.
     * @param $sql sql文
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return int $count 実行した行数
     */
    public static function exec(string $sql, string $dsn): int


    /*
     * プリペアドステートメントprepareを使う、クエリ結果を取得するselect文の処理.
     * @param $sql sql文.
     * @param $flg  $paramsを2次元配列として受け取る(insert,delete,updateなど複数文を処理する場合)か否か.
     * @param $params バインドするもの $params[] or $params[[]].
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return $flg===trueの場合は、2次元配列$dts[[]]. $dts、falseの場合は PDOStatement $stmt.
     */
    public static function prepare_select(string $sql, bool $flg, array $params , string $dsn)


    /*
     * プリペアドステートメントprepareを使う、データ取得を伴わないselect,insert,delete,updateクエリー実行、トランザクションをつかう.
     * @param $sql sql文.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @param array(array()) $paramsバインドするもの.
     * @return int  プリペアドステートメントを実行した回数.
     */
    public static function prepare_not_getdt(string $sql, array $params = [[]], string $dsn): int


    /*
     * sql文中の:name形式の値をプレイスフォルダーにバインドする.
     *
     * @param \PDOStatement
     * @parma $key
     * @param $val
     */
    protected static function create_bind(\PDOStatement $stmt, $key, $val)


    /*
     * 結果セットからカラムだけを配列に格納し返す.
     *
     * @param \PDOStatement
     * @return array
     */
    public static function fetch_col( \PDOStatement $stmt): array


    /*
     * PDOStatementからデータを取り出し、オブジェクト配列を生成する。
     *
     * @param \PDOStatement $stmt
     * @param string $cname_with_ns 完全修飾形式クラス名
     * @param FactoryClass $fs  Dependency injection.
     * @return array オブジェクト配列
     */
    public static function getObj(\PDOStatement $stmt, string $cname_with_ns, FactoryClass $fc): array


    /*
     * keyとvalueというPDOStatementオブジェクトに対して、連想配列としてとりだす.
     *
     * @param \PDOStatement $stmt
     * @return array $hash
     */
    public static function retHash(\PDOStatement $stmt): array
	
	
    /*
     * ステートメントによって直近の DELETE, INSERT, UPDATE 文によって作用したした行数をかえす.
     *
     * @param \PDOStatement $stmt.
     * @return int 直近の SQL ステートメントによって作用した行数.
     */
    public static function ret_cnt($stmt): int	


-------------------------------


Daoクラス

    /*
     * プリペアドステートメントprepareを使わない、クエリー実行.
     *
     * @param $sql sql文.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return PDOStatement.
     */
    public function use_query(string $sql, string $dsn = DSN1): \PDOStatement


    /*
     * プリペアドステートメントprepareを使わないsql文のデータselectに対する操作で、データをオブジェクトでかえす.
     *
     * @param string $sql prepareを使わないsql文select
	 * @param string $classname 取得するオブジェクトの完全修飾形式クラス名
     * @param string $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2.....
     * @return array object
     */
    public function get_objs(string $sql, string $classname, string $dsn=DSN1): array


    /*
     * プリペアドステートメントprepareを使わない、カラムだけを配列として、取得.
     *
     * @param $sql sql文.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return array 処理結果を配列として返す.
     */
    public function get_col(string $sql, string $dsn = DSN1): array


    /*
     * プリペアドステートメントprepareを使わないsql文のデータselectに対する操作で、keyと valueというテーブルのデータに対して連想配列でかえす.
     *
     * @param string $sql prepareを使わないsql文select
     * @param string $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2.....
     * @return array hash
     */
    public function getHash(string $sql, string $dsn = DSN1): array


    /*
     * プリペアドステートメントprepareを使う、クエリ結果を取得するselect文の処理.
     * @param $sql sql文.
     * @param $params バインドするもの $params[]1次元配列として指定.
     * @param $classname 名前空間名を含むクラス名
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return array classname objs
     */
    public function get_use_prepare(string $sql, array $params, string $classname, string $dsn = DSN1): array


    /*
     * プリペアドステートメントprepareを使う、カラムだけを配列として、取得.
     * @param $sql sql文.
     * @param array $params バインドするもの $params[[]] 2次元配列.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return 2次元配列
     */
    public function get_col_use_prepare(string $sql, array $params, string $dsn = DSN1): array


    /*
     * プリペアドステートメントprepareを使わない、データ取得を伴わないselect,insert,delete,updateクエリー実行.
     * @param $sql sql文
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @return int $count 実行した行数
     */
    public function use_exec(string $sql, string $dsn = DSN1): int


    /*
     * プリペアドステートメントprepareを使う、データ取得を伴わないselect,insert,delete,updateクエリー実行.
     * @param $sql sql文.
     * @param $dsn config.phpで使用する登録されているdsn名 DSN1 or DSN2......
     * @param $params array(array()) $paramsバインドするもの.
     * @return int  プリペアドステートメントを実行した回数.
     */
    public function ope_use_prepare(string $sql, array $params, string $dsn = DSN1): int

DaoクラスのメソッドはDBクラスのstaticメソッドを呼び出しております。アプリケーションでは、このDaoクラスを継承した、各DTOに対応したDaoクラスを定義し、このクラスのインスタンスを使用して、データを処理することにしております。一例として、データベースにおける、catテーブル、dtテーブルのDTO、DAOは以下のようになっております。

Catクラス(DTO)

CatDaoクラス(DAO)

Dtクラス(DTO)

DtDaoクラス(DAO)

オブジェクトとしての処理

sql文で結果を取得するという手法を取って行くとはいえ、やはり、オブジェクトとして処理を進めたほうが納得のいく場合もある。メジャーなフレームワークで取り入れている、オブジェクト関係マッピング (ORM: Object-relational mapping)を使うまでもなく、簡易なオブジェクト(オブジェクト配列)の処理の仕組みを定義したいと考えてみました。


 WEBROOT/news3/example/dao_api.php 

<?php

require_once '../../../news3/config.php';
require_once '../../../framework3/runfirst.php';


use framework3\common\FactoryClass;
use news3\models\Cat;
use news3\models\CatDao;


abstract class Dao_iterator implements \IteratorAggregate
{
    protected $dao;  # target dao class.
    protected $target_classname;  # target class name.

    protected $target_objs;  # picked up objects from database.
    protected $array_obj;  # for inserted ArrayObject.

    public function __construct($fc, $ns_dao_classname, $ns_target_classname)
    {
        $this->dao = $fc::createClass0($ns_dao_classname);
        $this->target_classname = $ns_target_classname;
    }

    // テーブル行データすべてを、オブジェクト配列としてとりこみ、外部イテレーター取得メソッドを実装したArrayObjectに代入する。
    public function getAll()
    {
        $classname = $this->pickupClassName($this->target_classname);
        $sql = "select * from ".strtolower($classname);
        $this->target_objs = $this->dao->get_objs($sql, $this->target_classname);
        $fc = FactoryClass::getNew();
        $this->setArrayObject($this->target_objs, $fc);

        return $this->target_objs;
    }

    // ネームスペースつきクラスネームからクラスネームだけをとりだす。
    private function pickupClassName($ns_classname)
    {
        return substr(strrchr($ns_classname, '\\'), 1);
    }

    // ターゲットとされるオブジェクト配列をArrayObjectに代入し、ArrayObjectをかえす。
    private function setArrayObject(array $objs, FactoryClass $fc): ArrayObject
    {
        if ($objs[0] instanceof $this->target_classname) {
            $this->array_obj = $fc::createClass1(ArrayObject::class);
            for ($i = 0; $i < count($objs); $i++) {
                $this->array_obj[] = $objs[$i];
            }
        } else {
            throw new \InvalidArgumentException('argument type invalid!');
        }
        return $this->array_obj;
    }


    public function addArrayObject($obj)
    {
        if ($obj instanceof $this->target_classname) {
            $this->array_obj[] = $obj;
        } else {
            throw new \InvalidArgumentException('argument type invalid!');
        }
    }

    public function retArrayObj(): ArrayObject
    {
        return $this->array_obj;
    }

    abstract function getIterator();
}


// catテーブル行データーに対して、イテレーター処理を実現するクラス。
class CatDao_iterator extends Dao_iterator
{
    public function getIterator()
    {
        // フィルタリングイテレーターを返す。array_obj->getIterator(): ArrayIterator
        return new CatIdFilterIterator($this->array_obj->getIterator());
    }
}

// spl定義済みのFilterIteratorを使用し、フィルタリングされたcatオブジェクトのイテレーターを実現するクラス。
class CatIdFilterIterator extends \FilterIterator
{
    public function __construct($iterator)
    {
        parent::__construct($iterator);  # フィルタリングの対象となるイテレータを引数に摂る。
    }

    // FilterIterator::accept() イテレータの現在の要素がフィルタを満たすかどうかを調べる
    public function accept()
    {
        // cat_idが2以上である場合は true(処理対象とする)
        $cat_obj = $this->current();
        return ($cat_obj->getCatId() > 1) ? true : false;
    }
}

// cliant
$fc = FactoryClass::getNew();
$catdao_iterator = new CatDao_iterator($fc, CatDao::class, Cat::class);
$catdao_iterator->getAll();

$catdao_iterator->addArrayObject(new Cat(3, 'cat3'));
$catdao_iterator->addArrayObject(new Cat(4, 'cat4'));

$array_obj = $catdao_iterator->retArrayObj();

$iterator = $catdao_iterator->getIterator();

// foreach を使用して反復処理を行う場合
echo "category name = ";
foreach ($iterator as $cat_obj) {
    echo $cat_obj->getCatName() . "\n";
}
echo "<br>";

// 上記の foreach 文で参照位置が終端まで来ているので、参照位置を初期化(先頭に戻す)する
$iterator->rewind();

echo "cat_id = ";
while($iterator->valid()) {
    $cat_obj = $iterator->current();
    echo $cat_obj->getCatId() . "\n";
    $iterator->next();
}


 output 
category name = faq cat3 cat4
cat_id = 2 3 4 

イテレーター(iterator)インターフェースはトラバーサル(Traversable) インターフェイスを継承しているため、これを実装したクラスはforeach を使った反復処理を使うことが出来ます。データベースから取得したデータをオブジェクト配列として取り込み、これを、反復処理可能な形にすることにより、データを操作するという流れになります。イテレーターアグリゲート(IteratorAggregate)インターフェースの実装は、phpであらかじめ定義されているspl イテレーターを使うことが出来、オブジェクト配列に対して、いろいろな操作を加えることを可能にしております。Dao_iteratorクラスでは、取り込まれた オブジェクト配列を反復処理可能なArrayObjectとしてとりこむ(setArrayObjectメソッド)ことにより、これを実現しております。

Dao_iteratorクラスは抽象クラスであり、これを継承したクラスをインスタンス化したもの( CatDao_iterator )を使用することになりますが、処理のメインはsplイテレーター(FilterIterator)を継承した、クラス(CatIdFilterIterator)に委ねられることになっております。

このような仕組みは、データベースから取り込まれたデータのみならず、データベース外からのデータをオブジェクト配列として取り込み、操作するというようなことを可能としております。そういう観点からすると、とても有用なものであると思われます。

尚、フレームワークではDaoIteratorクラスとして実装されております。実装コードは下記に示されております。


<参考サイト>

フレームワーク実装コード

DBクラス

framwork3/mvc/Db.php データベース接続操作クラス.

dbinit.php

APPNAME/dbinit.php Dbクラスに読み込まれるdsn情報を返す(sqlite).

Daoクラス

framework3/mvc/Dao.php sql文を使ってデータを操作するクラス.

DaoIteratorクラス

framework3/mvc/DaoIterator.php オブジェクト配列の反復処理を実現するクラス.

Catクラス

APPNAME/models/cat.php catテーブルDTDクラス

CatDaoクラス

APPNAME/models/catDao.php catテーブルDAOクラス

Dtクラス

APPNAME/models/Dt.php dtテーブルDTDクラス

DtDaoクラス

APPNAME/models/DtDao.php dtテーブルDAOクラス


☆ 関連ページは以下に記載されております。↓↓

phpフレームワーク自作連載目次

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください