はじめにの手順に従って正常に接続できた後は、このガイドを参照して、いずれかの PHP API を使用して行の書き込み、コミット、読み取りを行ってください。 各プログラムには独自のセットアップとクリーンアップが含まれています。 ある API から別の API へオプションをコピーするのではなく、タスク リンクを使用してワークフローを拡張してください。
接続テストの準備
既存の SQL Server テスト用データベースと、接続してローカルの臨時テーブルを作成できるログインを使用してください。 例はSQL認証を使っています。 対応する認証オプションやパスワードレスAzureオンボーディングについては、「サーバーへの接続」をご覧ください。
選択した拡張を読み込み、PHPプロセスの環境で SQL_SERVER、 SQL_DATABASE、 SQL_USER、 SQL_PASSWORD を設定します。
SQL_SERVER には、<server>,1433 のような明示的なポートを含めることができます。 サーバーはクライアントが信頼し、そのエンドポイントに有効な証明書を提示しなければなりません。 パスワードはプログラムやソース管理システムの外に保管してください。
実際のデータベース名、ユーザー名、パスワードを環境変数に保存します。 各プログラムは閉鎖ブレースを倍にし、PHPドライバーのODBCスタイルの接続処理のためにこれらの値を一度ブレースで囲みます。 エスケープは接続値の特殊文字を保持します。これはクエリパラメータ化ではありません。 詳細は「 サーバーへの接続」をご覧ください。
Transact-SQL (T-SQL) ステートメントは、#PhpGuide という一時テーブルを作成します。このテーブルは、それを作成した接続からのみ参照可能です。 プログラムは恒久的なアプリケーションテーブルを作成したり変更したりしません。 ワークフロー全体を通して同じ接続を保ちましょう。
行の書き込みと読み取り
プログラムは、パラメータを通じて整数、UTF-8 テキスト、SQL NULL を挿入します。 影響を受ける行が正確に1行であることを要求し、トランザクションをコミットし、返された値とPHP型を検証します。 最後のクエリでは、未完了のトランザクションが残っていないかどうかも確認します。
SQLSRV
この完全なプログラムを workflow-sqlsrv.phpとして保存し、 php workflow-sqlsrv.php実行してください。
<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException("Set $name before running this example.");
}
$config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$conn = sqlsrv_connect($config['SQL_SERVER'], [
'Database' => $config['SQL_DATABASE'],
'UID' => $config['SQL_USER'],
'PWD' => $config['SQL_PASSWORD'],
'Encrypt' => true,
'TrustServerCertificate' => false,
'LoginTimeout' => 30,
'CharacterSet' => 'UTF-8',
]);
if ($conn === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$stmt = null;
$inTransaction = false;
$failure = null;
try {
$stmt = sqlsrv_query($conn, 'CREATE TABLE #PhpGuide '
. '(Id int PRIMARY KEY, Title nvarchar(80), OptionalCount int NULL)');
if ($stmt === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
sqlsrv_free_stmt($stmt);
$stmt = null;
if (!sqlsrv_begin_transaction($conn)) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$inTransaction = true;
$id = 1;
$title = "Caf\u{00E9}";
$optionalCount = null;
$params = [
[&$id, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
[&$title, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('UTF-8'), SQLSRV_SQLTYPE_NVARCHAR(80)],
[&$optionalCount, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
];
$stmt = sqlsrv_prepare($conn,
'INSERT INTO #PhpGuide (Id, Title, OptionalCount) VALUES (?, ?, ?)', $params);
if ($stmt === false || !sqlsrv_execute($stmt)) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$affected = sqlsrv_rows_affected($stmt);
if ($affected === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
if ($affected !== 1) {
throw new RuntimeException('Expected exactly one inserted row.');
}
sqlsrv_free_stmt($stmt);
$stmt = null;
if (!sqlsrv_commit($conn)) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$inTransaction = false;
$stmt = sqlsrv_query($conn,
'SELECT Id, Title, OptionalCount, @@TRANCOUNT AS TransactionCount FROM #PhpGuide');
if ($stmt === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if ($row === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
if ($row !== ['Id' => 1, 'Title' => $title, 'OptionalCount' => null, 'TransactionCount' => 0]) {
throw new RuntimeException('Unexpected committed values, types, or transaction state.');
}
$end = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
if ($end === false) {
throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
if ($end !== null) {
throw new RuntimeException('Expected exactly one result row.');
}
} catch (Throwable $error) {
$failure = $error;
if ($inTransaction && !sqlsrv_rollback($conn)) {
$failure = new RuntimeException(
'Rollback failed: ' . print_r(sqlsrv_errors(), true), 0, $error);
}
throw $failure;
} finally {
if (is_resource($stmt)) {
sqlsrv_free_stmt($stmt);
}
try {
$cleanup = sqlsrv_query($conn, 'DROP TABLE IF EXISTS #PhpGuide');
if ($cleanup === false) {
throw new RuntimeException(
'Cleanup failed: ' . print_r(sqlsrv_errors(), true), 0, $failure);
}
sqlsrv_free_stmt($cleanup);
} finally {
sqlsrv_close($conn);
}
}
echo "Inserted 1 row; read committed UTF-8 text and null; no open transaction.", PHP_EOL;
PDO_SQLSRV
この別のプログラムを workflow-pdo.phpとして保存し、 php workflow-pdo.php実行してください。
<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException("Set $name before running this example.");
}
$config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
$config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$pdo = new PDO(
"sqlsrv:Server=$server;Database=$database;Encrypt=true;"
. "TrustServerCertificate=false;LoginTimeout=30",
$config['SQL_USER'],
$config['SQL_PASSWORD'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8,
PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
]
);
$stmt = null;
$failure = null;
try {
$pdo->exec('CREATE TABLE #PhpGuide '
. '(Id int PRIMARY KEY, Title nvarchar(80), OptionalCount int NULL)');
$pdo->beginTransaction();
$title = "Caf\u{00E9}";
$stmt = $pdo->prepare(
'INSERT INTO #PhpGuide (Id, Title, OptionalCount) VALUES (?, ?, ?)');
$stmt->bindValue(1, 1, PDO::PARAM_INT);
$stmt->bindValue(2, $title, PDO::PARAM_STR);
$stmt->bindValue(3, null, PDO::PARAM_NULL);
$stmt->execute();
if ($stmt->rowCount() !== 1) {
throw new RuntimeException('Expected exactly one inserted row.');
}
$stmt = null;
$pdo->commit();
$stmt = $pdo->query(
'SELECT Id, Title, OptionalCount, @@TRANCOUNT AS TransactionCount FROM #PhpGuide');
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row !== ['Id' => 1, 'Title' => $title, 'OptionalCount' => null, 'TransactionCount' => 0]) {
throw new RuntimeException('Unexpected committed values, types, or transaction state.');
}
if ($stmt->fetch(PDO::FETCH_ASSOC) !== false || $pdo->inTransaction()) {
throw new RuntimeException('Expected one result row and no open transaction.');
}
} catch (Throwable $error) {
$failure = $error;
if ($pdo->inTransaction()) {
try {
$pdo->rollBack();
} catch (PDOException $rollbackError) {
$failure = new RuntimeException(
'Rollback failed: ' . $rollbackError->getMessage(), 0, $error);
}
}
throw $failure;
} finally {
$stmt = null;
try {
$pdo->exec('DROP TABLE IF EXISTS #PhpGuide');
} catch (PDOException $cleanupError) {
throw new RuntimeException(
'Cleanup failed: ' . $cleanupError->getMessage(), 0, $failure ?? $cleanupError);
} finally {
$pdo = null;
}
}
echo "Inserted 1 row; read committed UTF-8 text and null; no open transaction.", PHP_EOL;
いずれのプログラムでも期待される出力:
Inserted 1 row; read committed UTF-8 text and null; no open transaction.
出力はチェックとクリーンアップが成功した後にのみ表示されます。 接続、書き込み、フェッチ、ロールバック、またはテーブルクリーンアップの失敗は、代わりに例外を発生させます。
選択肢を理解する
入力内容を T-SQL に連結するのではなく、値には ? のプレースホルダーを使用します。 例ではテーブル名と列名は固定されており、値パラメータは識別子の代わりにはなりません。 プリペアード ステートメントを使用することで、同じステートメントを異なる値で再度実行することもできます。 詳細については、「 クエリ実行関数の比較」をご覧ください。
| 選択肢 | 理由 |
|---|---|
| 明示的なトランザクションメソッド | この例では、書き込みを、コミットするかロールバックする必要のあるひとつの単位として扱っています。 SQLSRVは sqlsrv_begin_transaction、 sqlsrv_commit、 sqlsrv_rollbackを使用します。PDOは独自のトランザクションメソッドを使用します。 |
| SQLSRV parameter descriptors | 各ディスクリプタは方向、PHP型、SQL型を指定します。 参照は、null を格納する変数を含め、入力変数を保持します。 |
PDO bindValue |
このステートメントは、明示的な PDO パラメーター型を持つ各現在の値を受け取ります。
bindParam は、後続の実行で変数を参照渡しで読む必要がある場合に適しています。 |
| UTF-8 プラス nvarchar | PHPはUTF-8バイトを提供し、拡張機能とODBCドライバーはそれらをSQL Unicode列向けの形式に変換します。 フェッチされたテキストは元のPHP文字列とバイト単位で完全に一致しなければなりません。 |
| PDOの明示的な数値フェッチ | 結果比較ではPHP整数が期待されます。 PDO_SQLSRV属性がない場合、これらのSQL整数結果はドライバーのデフォルトの文字列表現を使用します。 |
| 名前付きフェッチキー |
SQLSRV_FETCH_ASSOC と PDO::FETCH_ASSOC は列名でインデックスされた配列を返します。 API定数は相互に置き換えて使用できません。 |
PDO::exec テーブル設定用 |
テーブル作成文を直接実行します。 このアプローチにより、サーバー準備済みステートメントのスコープ内で一時テーブルを作成することを回避できます。 |
| 順方向のみの結果 | プログラムは各行を一度ずつ読み込みます。 バッファリングされた結果を要求したり、SELECT行数をフェッチ成功の証明として使ったりしません。 |
SQLSRVは、配列フェッチが終端に達するとfalseを返し、フェッチ エラー時にはnullを返します。 PDO のフェッチは、最後に false を返します。例外モードが構成されている場合、データベースのフェッチ エラーが発生すると例外がスローされます。 両方のAPIにおいて、 OptionalCount フィールド自体にPHP nullが含まれています。 そのフィールド値は結果の終了を示すマーカーではありません。
書き込みは検証クエリの前にコミットします。 後に行われる読み取りが失敗した場合、それ以前にコミットされた書き込みはロールバックできません。 この例では、その行はクリーンアップがドロップする一時テーブルにのみ属します。 恒久的なテーブルがあるアプリケーションでは、失敗を正確に報告し、不確かなコミット結果の後にインサートを盲目的に繰り返さないでください。
エラー処理とクリーンアップ
catch ブロックは、アクティブなトランザクションをロールバックし、失敗を再スローします。
finallyブロックはステートメントを解放し、一時テーブルをドロップし、接続を解放します。 クリーンアップは、成功パスと失敗パスの両方で実行されます。 ロールバックやテーブルのクリーンアップも失敗した場合、プログラムは元の例外を以前の例外として保持します。
コマンドラインでの演習の場合、未処理の例外が発生すると、PHP は 0 以外の終了コードとエラー メッセージを表示して終了します。 ウェブアプリケーションでは、リクエストを処理するコードでキャッチします。 診断をアプリケーションログに記録する前に、敏感な値を削除し、適切なエラー応答を返してください。 接続情報、認証情報、生のデータベースエラーをブラウザに公開しないでください。
| Failure | 確認すべきこと |
|---|---|
| 拡張機能またはドライバーがありません | PHPプロセスが、一致する実行環境と設定で意図した拡張機能を読み込むことを確認します。 動作するコマンドラインのPHP構成が、あなたのウェブサーバーがそれを使っている証拠にはなりません。 |
| 接続または証明書のエラー | クエリコードを変更する前に、エンドポイント、認証、権限、クライアント証明書の信頼を確認しましょう。 本番環境での証明書の検証を無効にしないでください。 |
| 一時的なテーブルが見つかりません | セットアップ、書き込み、読み込みは一つの接続で行う。 PDOの設定では、デフォルトで準備されたexecコールを置き換えるのではなく、示されたqueryパスを保持してください。 |
| 予期せぬ値またはPHP型 | fetch属性とパラメータディスクリプタを確認してください。 その違いを隠す目的で、厳密なアサーションを大まかな比較に置き換えないでください。 |
| 書き込みまたはロールバックの失敗 | SQLSRV のエラー、または PDO の例外を即座にキャプチャしてください。 ロールバックの失敗も報告してください。ロールバックに失敗した場合は、書き込みが元に戻されたとみなさないでください。 |
| 接続切断後のクリーンアップ失敗 | プログラムはサーバー側のドロップを確認できません。 PHPハンドルを解放しても、基礎となるプールされた物理接続がすぐに閉じられたとは限りません。 |
PHP拡張はバインディングとフェッチAPIを提供します。 ODBCはサーバーと通信し、接続プーリングを管理します。 SQL Serverは一時テーブルのスコープ、トランザクション、制約、コミットされたデータを制御します。 物理接続のクローズによるクリーンアップに頼るのではなく、テーブルを明示的にドロップしましょう。
演習の終了後、workflow-sqlsrv.php と workflow-pdo.php が不要になった場合は削除してください。 テストが終了したら、テスト プロセスから資格情報に関する環境変数を削除してください。 どちらのプログラムも、削除すべき永続的なテーブルを残しません。
これらのウォークスルーはSQL Serverを対象としています。 Azure SQL Database、Azure SQL Managed Instance、Azure Synapse Analytics、FabricのSQL Database、Warehouse in Fabricについては、まずターゲットの認証とT-SQLのサポートを確認してください。 両方の Fabric 製品には Microsoft Entra ID 認証が必要であり、Warehouse では複数のアクティブ結果セット (MARS) を無効にする必要があります。 これらのSQL Serverプログラムは、サービス固有のトランザクションや一時テーブルのサポートを確立していません。
次のタスクを選択する
必要な動作に関する記事を活用して、ワークフローを拡張してください。
| Task | Guidance |
|---|---|
| 認証を選ぶか、接続を管理するかを選びます | サーバーに接続中。 |
| 異なる値でプリペアドステートメントを再利用する | クエリ実行関数を比較してください。 |
| PDOの直接実行かプリペアド実行かを選択 | PDO_SQLSRV ドライバーでの直接ステートメント実行と準備済みステートメント実行 |
| 値の取得と結果形状の選択 | データを取得中。 |
| 結果をスクロールまたはバッファします | カーソルの種類を指定し、行を選択すること。 |
| アプリケーションデータを変更する | データの更新. |
| PHP および SQL の型変換を制御する | データ型の変換。 |
| 障害の診断とドライバーログの有効化 | エラーや警告の処理、ログの記録。 |
| 機密性の高い列を保護します | SQL Server 用 Microsoft PHP ドライバーで Always Encrypted を使用する。 |
| 運用環境のアプリケーションを堅牢化する | Microsoft Drivers for PHP for SQL Serverのセキュリティ上の考慮事項 |