WEBシステムの開発プラットフォーム LIB PHP Framework! 高速でセキュアなフルスクラッチ開発を今すぐ開始できます。

PHP基礎学習

Chapter7. データベースアクセス

Webアプリでは、HTMLフォームに入力されたデータをサーバーに保存し必要に応じて出力します。
例えば、会員登録フォームでは会員情報を入力させ、管理ページでは登録した会員リストを表示します。 このとき、入力されたデータはRDB(リレーショナルデータベース)という データ構造で保存しますが、本章ではこのRDBの基本構造と取り扱いについて学習します。

1. RDBとRDBMS

RDB(リレーショナルデータベース)は表形式のデータ構造で、顧客情報や購買情報などを取り扱うWebアプリでは重要なデータの保存方法です。
表形式とはExcelスプレッドシートと同じで、横軸に列、縦軸に行を配置します。
上図は、顧客情報を保存するためのデータ構造を表しています。
表形式のデータ構造では、列をcolumn(カラム)またはfield(フィールド)といい、行をrow(ロウ)またはrecord(レコード)といいます。
列は必要に応じて、住所やE-mailアドレスなどの項目を定義します。(列を増やします。) 行には一人の顧客情報を一行に追加していきます。 このようなデータ構造をDataTable(データテーブル)といいます。
実はExcelスプレッドシートもデータベースなのです。 Excelでは、データを行列に配置しExcelバイナリブックとして保存することで、データベースの役割をします。
上図は、顧客情報テーブルに関連する、購買情報を保存するテーブルを定義したものです。
顧客情報テーブルのCIDカラムや購買情報テーブルのOrderIDカラムは、プライマリーキーといってテーブル内のレコードを一意に識別するためのものです。
顧客情報と購買情報は1対多の関係にありますので、顧客情報テーブルのプライマリーキー「CID」を購買情報テーブルに持たせることで、2つのテーブルを結合して情報を取り出すことができるようになります。

RDBにデータを保存したり取り出すためには、SQL(エスキューエル)という命令文を発行します。
そしてこのSQLを解釈し、データの挿入、更新、削除、取得を行うのがRDBMS(リレーショナルデータベースマネジメントシステム)です。 RDBMSには次のようなソフトウェアがあります。

Microsoft(R) SQL Server(エスキューエルサーバー)
Oracle(オラクル)
MySQL(マイエスキューエル)
SQLite(エスキューライト)

このうち、LIB PHP FrameworkではMySQLとSQLiteを使用しますが、MySQLがデータベースサーバーシステムであるのに対し、SQLiteはExcelやAccessのように複数のテーブルをひとつのファイルに保存する、ファイルベースのシステムです。

2. SQL

SQLとはデータを操作するためのデータベース言語です。
実際に、データベースにデータを挿入、更新、削除、取得する命令文を見ていきましょう。
それぞれの命令文は単なる文字列ですが、データベース操作においてはクエリ(query)と呼びます。
Insert(挿入クエリ)
Insert Into t_customer(CID, name, tel) Values(1, '鈴木', '080-0000-1111');
Insert Into t_customer(CID, name, tel) Values(2, '田中', '080-0000-2222');

前項の顧客情報テーブルを「t_customer」と命名し、カラムに「CID」「name」「tel」を定義しています。
クエリはセミコロンで一文を終了させます。 上記の例では2行のデータが追加(挿入)されます。
テーブル名の後の括弧内に、保存したいカラムをカンマ区切りで指定し、Valuesの後の括弧内に対応するデータを記述します。

Update(更新クエリ)
Update t_customer Set name = 'スズキ', tel = '080-0000-1234' Where (CID = 1);

更新クエリは、Setの後に更新したいデータを「カラム=データ」の書式でカンマ区切りで記述します。
シングルコーテーションは文字列データを表します。シングルコーテーションで囲まれないデータは数値として解釈されます。
更新クエリでは更新先の行特定するために、Where句につづけて条件式を指定します。 上記の例ではCIDが「1」に該当する行のデータが更新されます。
条件式を囲むと見通しが良くなりますので、統一して条件式は括弧で囲むと覚えておきましょう。

Delete(削除クエリ)
Delete From t_customer Where (CID = 1) Limit 1;

削除クエリでは条件式に該当する行が削除されますが、上記の例ではLimit区を付け削除対象を1行に限定しています。
こうすることで、ミスによって多数のデータを喪失しないようにします。

Select(選択クエリ1)
Select * From t_customer Where (CID = 1);
Select name, tel From t_customer Where (CID = 1);

Selectに続く「*」はすべてのカラムを意味します。 特定のカラムのみ取得したい場合にはカラム名をカンマ区切りで指定します。

Select(選択クエリ2 条件式)
Select CID, name, tel From t_customer Where (tel Like '080%');

上記の例では、条件式に「telが080で始まる場合」を指定しています。
条件に該当する複数行のデータを取得できます。

Select(選択クエリ3 結合)
Select o.*, c.CID, c.name From t_order As o Left Join t_customer As c On (o.CID = c.CID)
Where (o.tm_order >= '2021/10/11');

上記例では、前項の購買情報テーブルを「t_customer」と命名し、結合しています。
Join句は複数のテーブルを結合するための構文です。「t_order As o Left Join t_customer As c On (o.CID = c.CID)」の部分が結合の構文です。
Fromの後には通常、単一のテーブルを指定しますが、Join句を指定することで複数のテーブルを結合した結果を取得することができます。

Left Joinは左結合といって、左のテーブルをベースに右のテーブルを結合します。 As句はテーブルやカラムに別名を付けるための構文で、クエリ全体で別名を使用して見通しを良くする為に使用します。
Join句の左右に結合するテーブルを指定し、Onの後にで結合条件を記述します。「(o.CID = c.CID)」の部分が結合条件です。 t_customerのプライマリーキーをt_orderに持たせていますのでこのように指定します。
結合条件はWhere句と同様、ひとつの条件を括弧で囲むと見通しが良くなります。
1対多の関係にあるテーブルは、上記のようにして結合することで情報が欠落することなく該当するすべての行を取得することができます。
左結合は左のテーブルからすべての行を取得し、結合条件に基づいて右のテーブルから該当する行だけを結合します。 右のテーブルに該当する行がない場合にはnullとなります。

左右の位置関係を誤ると、必要な情報が欠落しますので注意しなければなりません。 基本的には1対多の関係では左に多のテーブルを配置すると間違いありません。
3つ以上のテーブルを結合する場合も、Left Joinを使用し、左へ左へ結合するのが基本形です。

3. データベース接続とデータ操作

それでは、実際にPHPコードでデータベースに接続し、データを操作してみましょう。
データベースに接続するまえに、MySQLデータベースにデータテーブルを作成しておかなければなりません。 テーブルの作成については、How to useのChapter4. phpMyAdminによるテーブル作成を参照してください。

会員情報の登録

まず、Web Studioで会員情報入力フォームを作成し、登録ボタンクリック時にAjaxにてフォームデータを送信します。
次に、イベント編集画面でPOSTイベントを追加し、下記コードを記述します。

Ajaxによるバックグラウンド通信とPOSTイベントについては、How to useのChapter3. ロードイベントとセッション管理を参照してください。

$dac = new DAC(Lib::connection(), "mysql");
try {
$dac->open();
$dac->command = "Insert Into t_customer(CID, name, tel) Values({$_POST["CID"]}, '{$_POST["name"]}', '{$_POST["tel"]}');";
$dac->execute();

$this->response("", true);
} catch (Exception $ex) {
$this->response($ex->getMessage(), false);
}

データベースへの接続にはDACクラスを使用します。
DACクラス呼び出し前に、イベント編集画面でライブラリ「DAC」と「Lib」を選択してください。
DADコンストラクタには、Libクラスのconnection()と文字列「mysql」を渡します。 これでMySQLデータベースへの接続準備ができました。

データベースやファイルなどのリソースへアクセスする際は、必ずtry-catchブロックで囲みエラー処理記述します。
tryブロック内でエラーが発生した場合には、catchブロック内の処理が実行されます。 catchブロックではエラーの内容をExceptionオブジェクトで受け取ることができます。
上記の例では、ExceptionクラスのgetMessage()でエラーメッセージを取得しHTTPレスポンスとして返します。

データベースへの接続はDACクラスのopen()を実行するのみです。
次に、commandプロパティに挿入クエリを代入し、execute()を実行することでデータ操作は完了です。
その後の処理がなければ、Pageクラスのresponse()で終了です。 データベース接続は自動的に終了します。

$dac = new DAC(Lib::connection(), "mysql");
try {
$dac->open();
$dac->command = "Update t_customer Set name = '{$_POST["name"]}', tel = '{$_POST["tel"]}' Where (CID = {$_POST["CID"]});";
$dac->execute();

$this->response("", true);
} catch (Exception $ex) {
$this->response($ex->getMessage(), false);
}

上記の例は、顧客情報を変更するために更新クエリを実行しています。 手順は挿入クエリと同様です。
しかし実際には、CIDが登録済みの場合は更新し、未登録の場合は更新しなければなりません。
そこでフレームワーク関数を使用し、下記のように追加と更新を一度に行います。

$dac = new DAC(Lib::connection(), "mysql");
try {
$dac->open();
// note: CIDの該当する行数を数える
$dac->setColumn("Count(*)");
$dac->setOption("CID = [0]", $_POST["CID"]);
$count = $dac->scalar("t_customer", true);

// note: 挿入および更新データをセットする
$dac->clear();
$dac->setData("name", $_POST["name"]);
$dac->setData("tel", $_POST["tel"]);

// note: 結果が1件以上の場合には更新クエリを実行
if ($count) {
$dac->setOption("CID = [0]", $_POST["CID"]);
$dac->update("t_customer", 1);
} else {
$CID = $dac->setSequence("t_customer_seq");
$dac->setData("CID", $CID);
$dac->insert("t_customer");
}
$this->response("", true);
} catch (Exception $ex) {
$this->response($ex->getMessage(), false);
}

まず、POST送信されたCIDが保存されているかどうかをSQL集計関数「Count」をカラムにして問い合わせます。
DACクラスのscalar()は、選択クエリを実行して1行1列の結果を取得します。

次に挿入または更新するデータをDACクラスのsetData()でセットします。

そして、CIDが登録済みの場合にはDACクラスのupdate()で更新クエリを実行します。
未登録の場合には、DACクラスのsetSequence()を実行して新しいIDを作成して、insert()で挿入クエリを実行します。

会員リストの表示

管理画面などに登録された会員情報をリスト表示する場合には、Web Studioのイベント編集画面でロードイベントを追加し次のように記述します。

$dac = new DAC(Lib::connection(), "mysql");
try {
$dac->open();
$dac->setOption("tel Like '080%'");
$dac->setColumn("CID, name, tel");
$rows = $dac->select("t_customer");
} catch (Exception $ex) {
$this->errors["message"] = $ex->getMessage();
} finally {
$dac->close();
}

foreach ($rows as $row) {
$html.= "<tr>";
$html.= " <td>{$row["name"]}</td>";
$html.= " <td>{$row["tel"]}</td>";
$html.= " <td><button class="detail" data-cid="{$row["CID"]}">詳細<button></td>";
$html.= "</tr>";
}
$this->values["html"] = $html;

DACクラスのselect()メソッドは、setOption()でセットされた条件とsetColumn()でセットされたカラムを組立て選択クエリを発行します。
返された$rowsは行配列です。 練習課題(T1) 制御構文によるデータ出力で作成した連想配列と同様ですので、ループしてHTMLタグを出力しましょう。

ここで重要ななのがfinallyブロックです、finallyブロックはtry-catchブロックでエラーが発生した場合にも必ず実行されます。
finallyブロックではリソースの解放を行います。 前述のPOSTイベントでは、Pageクラスのresponse()でPHPの実行を終了することでデータベース接続が終了しましたが、
上記の例では、データ取得後に別の処理が続きますので、処理を続ける前にDACクラスのclose()を呼び出すことでデータベース接続を終了させます。

4. 比較演算子と論理演算子

Where区や結合条件では、下記の比較演算子と論理演算子が使用できます。

> 大きい
< 小さい
>= 大きい、もしくは等しい
<= 小さい、もしくは等しい
!=
<>
等しくない
() AND () 左右の両方の条件に一致
() OR () 左右どちらかの条件に一致
NOT () 括弧内の条件に一致しない
BETWEEN a AND b a と b の範囲内
IN ('文字列1', '文字列2', '文字列3')
IN (1, 2, 3)
カンマ区切りのデータに一致
LIKE '文字列%'
LIKE '%文字列'
LIKE '%文字列%'
前方一致
後方一致
前後方一致

顧客リストの検索などでは、複数の条件を組み合わせ検索します。
AND検索なのかOR検索なのか設計次第ですが、下記のような基準を覚えておくとよいでしょう。

日付選択 AとBの範囲を指定する
チェックボックス 複数のデータに一致させる
ラジオボタン、セレクトボックス 単一のデータに一致させる
文字列 前後方に一致させる

検索欄は、およそこのようなパターンの組み合わせになりますが、選択クエリでは下記のように記述します。

$dac = new DAC(Lib::connection(), "mysql");
try {
$dac->open();

// note: AとBの範囲を指定する
$tmStart = strtotime($_POST["tm_start"]);
$tmEnd = strtotime("+1 day", strtotime($_POST["tm_start"]));
$dac->setOption("(tm_entry >= [0]) And (tm_entry < [1])", date("Y-m-d", $tmStart), date("Y-m-d", $tmEnd));

// note: 複数のデータに一致させる
$csv = implode("','", $_POST["pref"]);
$dac->setOption("pref In ('{$csv}')");

// note: 単一のデータに一致させる
$dac->setOption("gen = [0]", $_POST["gen"]);

// note: 前後方に一致させる
$dac->setOption("(name LIke '%{$_POST["name"]}%') Or ((kana LIke '%{$_POST["name"]}%'))");

$dac->setColumn("*");
$rows = $dac->select("t_customer");
} catch (Exception $ex) {
$this->errors["message"] = $ex->getMessage();
} finally {
$dac->close();
}

DACクラスのsetOption()で条件を追加すると、デフォルトではそれぞれの条件がANDで連結されます。
このコードで生成される選択クエリは下記のようになります。

Select * From t_customer 
Where ((tm_entry >= '2021/10/01') And (tm_entry < '2021/11/01'))
And (pref In ('東京都', '神奈川県', '埼玉県', '千葉県'))
And (gen = '1')
And ((name Like '%鈴木%') Or (kana Like '%鈴木%'))

この選択クエリは、10月に登録した東京近郊に在住の男性で鈴木さんを検索しています。
ORでnameカラムとkanaカラムを条件指定していますので、$_POST["name"]が「鈴木」でも「スズキ」でも該当のデータを検索できるというわけです。
このようにして、ANDは縦にORは横に記述することで、間違いのない検索結果を得られます。

5. 検索結果のページング

選択クエリの結果が数百件あるリストでは、ページ分割して表示させますが、Pagerクラスを使用することで簡単に実装できます。
Pagerクラス呼び出し前に、イベント編集画面でライブラリ「Pager」を選択し、Propertiesに「pager」を入力し保存してください。
次に、イベント編集画面でロードイベントを追加して下記のコードを記述します。

$dac = new DAC(Lib::connection(), "mysql");
$pager = new Pager();
$pager->limit = 100;
$pager->page = ($_GET["page"]) ? $_GET["page"] : 1;

$dac->setColumn("CID, name, tel");
$dac->selectPageRows("t_customer", $rows, $pager) or $this->errors["message"] = $dac->message;

foreach ($rows as $row) {
$html.= "<tr>";
$html.= " <td>{$row["name"]}</td>";
$html.= " <td>{$row["tel"]}</td>";
$html.= " <td><button class="detail" data-cid="{$row["CID"]}">詳細<button></td>";
$html.= "</tr>";
}
$this->values["html"] = $html;
$this->pager = $pager;

DACクラスのselectPageRows()は、第3引数に渡されたPagerクラスのインスタンスを使用して、特定の範囲のデータを第2引数にセットします。
このような渡したはずの引数「$rows」に、関数がデータをセットしてくれる動作を「リファレンス渡し」といいます。
また、selectPageRows()は、関数内部でtry-catchを実装しており、自動的にデータベースに接続し、データを取得後にはデータベース接続を終了します。
データベース接続中にエラーが発生した場合には、selectPageRows()はエラーメッセージをmessageプロパティにセットしfalseを返します。
ですので、上記の例ではselectPageRows()につづけてor演算子で、結果がfalseの場合にエラーメッセージの代入を行っています。

<table>
<thead>
<tr>
<td>氏名</td>
<td>TEL</td>
<td></td>
</tr>
</thead>
<tbody>
<?= $page->values["html"] ?>
</tbody>
</table>

<div class="pager">
<?= $page->pager->getUL() ?>
</div>

HTMLエディターには上記のように記述することでページングタグを表示させます。
ページクラスの「pager」は追加のプロパティです。
ページクラスのインスタンス「$page」にはこのようなプロパティは存在しませんが、イベント編集画面の「Properties」入力することでプロパティを追加できます。