PHPでMySQLを操作するとは、PHPからデータベースに接続し、データの登録(Create)・取得(Read)・更新(Update)・削除(Delete)の4操作(CRUD)を行うことです。安全に実装する基本は、接続にPDOを使い、SQLは値を直接埋め込まずprepared statement(プレースホルダ)で実行することです。
Webアプリの中身は、突き詰めれば「データを保存して、取り出して、書き換えて、消す」の繰り返しです。会員管理も、商品一覧も、投稿機能も、すべてこのCRUDの組み合わせでできています。逆に言えば、CRUDをPDOで安全に書けるようになれば、たいていの動的サイトの土台は作れます。
この記事では、PHP+MySQLでCRUDを一から実装する手順を、動くコードと「なぜそう書くか」をセットで解説します。SQLインジェクションを防ぐprepared statementの使い方、複数の更新を安全にまとめるトランザクション、文字化けやつまずきの対処まで、実務でそのまま使える形でまとめます。最後に、ここからログイン認証や検索機能へ広げる次の一手も案内します。
PHPでMySQLを操作するとは(PDOとmysqliの選択)
PHPからMySQLを操作する方法は主に2つ、PDOとmysqliがあります。本記事ではPDOを使います。理由は、対応データベースが幅広く(将来PostgreSQLなどへ移植しやすい)、名前付きプレースホルダが使えてコードが読みやすいためです。PDO全体の仕様はPHP公式のPDOマニュアルにまとまっており、迷ったときの一次情報として手元に置いておくと安心です。
| 観点 | PDO | mysqli |
|---|---|---|
| 対応DB | MySQL・PostgreSQLなど多数 | MySQLのみ |
| プレースホルダ | 名前付き(:name)が使える | 基本は「?」のみ |
| 移植性・可読性 | 高い | 中 |
| 本記事の採用 | ◎ こちらを使う | — |
どちらでもCRUDは実装できますが、これから学ぶなら名前付きプレースホルダで意図が明確になるPDOがおすすめです。バックエンド全体の中でデータベースがどの位置にあるかは、バックエンドとは何かの解説で整理できます。
事前準備:テーブルとサンプルデータ
CRUDを試すためのテーブルを用意します。今回は商品を管理するitemsテーブルを例にします。ローカルで試す場合は、XAMPPやMAMPなどPHPとMySQLが動く環境を準備してください。
CREATE TABLE items (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); idはAUTO_INCREMENTで自動採番、nameは商品名、priceは価格、stockは在庫数です。この4つのカラムに対して、登録・取得・更新・削除を行っていきます。
接続を共通化する(db.php)
CRUDの各ファイルから毎回接続コードを書くのは無駄なので、接続処理をdb.phpにまとめ、各ファイルから読み込む形にします。接続にはPDOを使います。
<?php
// db.php : データベース接続を共通化する
$host = 'localhost';
$dbname = 'shop';
$dbuser = 'db_user'; // 環境に合わせて変更
$dbpass = 'db_password'; // 環境に合わせて変更
$charset = 'utf8mb4';
$dsn = "mysql:host={$host};dbname={$dbname};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // エラーを例外で受け取る
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 連想配列で取得
PDO::ATTR_EMULATE_PREPARES => false, // 本物のprepared statement
];
try {
$pdo = new PDO($dsn, $dbuser, $dbpass, $options);
} catch (PDOException $e) {
// 本番では詳細を画面に出さず、ログに記録する
exit('データベースに接続できませんでした');
} 3つの接続オプションには意味があります。ERRMODE_EXCEPTIONはエラー時に例外を投げる設定で、失敗に気づきやすくなります。FETCH_ASSOCは取得結果を連想配列で返す設定。EMULATE_PREPARES => falseは、PHPの擬似処理ではなくMySQL本来のprepared statementを使う設定で、後述するSQLインジェクション対策をより確実にします。charset=utf8mb4は文字化け防止に必須です。
以降のCRUDコードは、すべて先頭でrequire 'db.php';を読み込み、$pdoを使う前提で進めます。
Create:データを登録する(INSERT)
まずはデータの登録です。INSERT文をprepared statementで実行します。値は:nameのようなプレースホルダにし、execute()に配列で渡します。
<?php
require 'db.php';
$sql = 'INSERT INTO items (name, price, stock)
VALUES (:name, :price, :stock)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => 'コーヒー豆',
':price' => 1200,
':stock' => 30,
]);
// 直前に追加した行のIDを取得できる
$newId = $pdo->lastInsertId();
echo "登録しました(ID: {$newId})"; 登録後にlastInsertId()で採番されたIDを取得できます。登録した商品の詳細ページへリダイレクトしたいときなどに便利です。フォームから受け取った値を登録する場合も、必ずプレースホルダ経由で渡します(値を文字列連結でSQLに埋め込まない)。
Read:データを取得する(SELECT)
取得には2パターンあります。一覧をまとめて取る場合と、IDなどの条件で1件取る場合です。まず一覧取得です。条件のない単純なSELECTはquery()で実行し、fetchAll()で全行を配列で受け取ります。
<?php
require 'db.php';
$stmt = $pdo->query('SELECT * FROM items ORDER BY id DESC');
$items = $stmt->fetchAll();
foreach ($items as $item) {
// 出力時はXSS対策でエスケープする
echo htmlspecialchars($item['name'], ENT_QUOTES, 'UTF-8');
echo ' / ' . (int)$item['price'] . '円<br>';
} 次に、IDを指定して1件取得するパターン。ここでは外部から渡される値($_GET)を条件に使うため、query()ではなくprepared statementを使うのが鉄則です。取得結果はfetch()で1行受け取ります。
<?php
require 'db.php';
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM items WHERE id = :id');
$stmt->execute([':id' => $id]);
$item = $stmt->fetch();
if ($item === false) {
exit('該当する商品が見つかりません');
}
echo htmlspecialchars($item['name'], ENT_QUOTES, 'UTF-8'); 外部からの値を条件に使うときは必ずprepared statement——これがCRUDで最も繰り返し出てくる原則です。fetch()はデータがなければfalseを返すので、存在チェックを入れておくと安全です。
Update:データを更新する(UPDATE)
更新はUPDATE文を使います。必ずWHEREで対象を絞るのが最重要ポイントです。WHEREを書き忘れると全行が更新されてしまうため、削除と並んで事故の起きやすい操作です。
<?php
require 'db.php';
$sql = 'UPDATE items
SET price = :price, stock = :stock
WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':price' => 1300,
':stock' => 25,
':id' => 1,
]);
// 実際に更新された行数を確認できる
echo $stmt->rowCount() . '件を更新しました'; rowCount()で実際に更新された行数を取得できます。0が返る場合は、対象IDが存在しないか、値が元と同じで変化がなかったケースです。更新フォームから値を受け取る場合も、すべてプレースホルダ経由で渡します。
Delete:データを削除する(DELETE)
削除はDELETE文です。UPDATEと同様、WHEREで対象を必ず限定すること。WHEREのないDELETEは全件削除になり、取り返しがつきません。
<?php
require 'db.php';
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare('DELETE FROM items WHERE id = :id');
$stmt->execute([':id' => $id]);
echo $stmt->rowCount() . '件を削除しました'; 削除は通常、確認画面やPOSTリクエストを経由して実行します。リンク(GET)一発で削除できる作りにすると、クローラーや先読みで誤削除が起きたり、CSRFの標的になったりします。削除のような変更操作はPOSTで受け、必要に応じてトークンで保護します(CSRF対策はPHPログイン機能の作り方で詳しく解説しています)。
SQLインジェクションを防ぐ(prepared statementの仕組み)
CRUDを通して繰り返し使ってきたprepared statementは、SQLインジェクション対策の核心です。改めて、危険な書き方と安全な書き方を並べて、なぜ防げるのかを確認します。
<?php
// ❌ 危険:入力値を直接SQLに連結している
$name = $_POST['name'];
$pdo->query("INSERT INTO items (name) VALUES ('{$name}')");
// name に '); DROP TABLE items; -- のような値を入れられると危険 <?php
// ◎ 安全:値はプレースホルダで渡す
$stmt = $pdo->prepare('INSERT INTO items (name) VALUES (:name)');
$stmt->execute([':name' => $_POST['name']]);
// 値はSQLの一部としてではなく「データ」として扱われる prepared statementでは、SQLの構文(テンプレート)と値が分離して処理されます。プレースホルダに渡された値はあくまで「データ」として扱われ、SQLコマンドとして解釈されません。だから、値の中にSQLらしき文字列が含まれていても注入が成立しないのです。db.phpでEMULATE_PREPARES => falseにしておくと、この分離がMySQL側で確実に行われます。プレースホルダの詳しい挙動や使い方は、PHP公式マニュアルのプリペアドステートメント解説も参照してください。なお、テーブル名やカラム名はプレースホルダにできない点に注意が必要です。これらを動的に変えたい場合は、あらかじめ許可リスト(ホワイトリスト)で受け付ける値を限定し、入力値をそのままSQLに使わないようにします。
例外処理とトランザクション(不整合を防ぐ)
実務では「複数のテーブルをまとめて更新する」場面が出てきます。たとえば「在庫を1減らす」と「注文を1件追加する」はセットで、片方だけ成功すると在庫数が狂います。こうした処理をすべて成功するか、すべて取り消すかに揃えるのがトランザクションです。
<?php
require 'db.php';
try {
$pdo->beginTransaction(); // トランザクション開始
// 在庫を1減らす
$stmt = $pdo->prepare('UPDATE items SET stock = stock - :qty WHERE id = :id');
$stmt->execute([':qty' => 1, ':id' => 1]);
// 注文を1件追加する
$stmt = $pdo->prepare('INSERT INTO orders (item_id, qty) VALUES (:id, :qty)');
$stmt->execute([':id' => 1, ':qty' => 1]);
$pdo->commit(); // 両方成功 → 確定
} catch (PDOException $e) {
$pdo->rollBack(); // どちらか失敗 → すべて巻き戻す
exit('処理に失敗しました');
} beginTransaction()で開始し、すべて成功したらcommit()で確定、途中で例外が出たらrollBack()で開始前の状態に戻します。これにより「在庫だけ減って注文が記録されない」といった中途半端な状態を防げます。db.phpでERRMODE_EXCEPTIONを設定しているからこそ、失敗をcatchで受け取れる点も重要です。
文字化け・つまずき対策
CRUD実装でハマりやすいポイントを先回りでまとめます。多くは接続設定とWHERE忘れに集約されます。
| 症状 | 主な原因 | 対処 |
|---|---|---|
| 日本語が「???」になる | 接続のcharset未設定 | DSNにcharset=utf8mb4/テーブルもutf8mb4で作成 |
| 更新・削除が全行に及んだ | WHEREの書き忘れ | UPDATE/DELETEは必ずWHEREで対象を限定 |
| エラーが出ず原因不明 | ERRMODEが例外でない | ERRMODE_EXCEPTIONを設定して例外で気づく |
| 数値がおかしい・型が不安定 | 入力値を未整形で使用 | 数値は(int)でキャストしてから使う |
特にWHERE忘れは、開発中に一度はやりがちな事故です。UPDATEとDELETEを書くときは「WHEREを先に書く」癖をつけると安全です。
動作確認:実際に動かして検証する
コードを書いたら「書いた」で終わらせず、実際に動かして想定通りかを確認します。ローカル環境で、CRUDをひと通り通してチェックしてください。
- Create:登録後、phpMyAdminなどで
itemsテーブルに行が増えているか - Read(一覧):登録した商品が一覧に表示されるか
- Read(1件):
?id=1のように指定して、該当商品だけ取得できるか。存在しないIDで「見つかりません」になるか - Update:価格や在庫が変わり、
rowCount()が更新件数を返すか - Delete:対象だけが消え、他の行が残っているか
- トランザクション:わざと2つ目のSQLを失敗させ、1つ目の更新も巻き戻る(rollbackされる)か
6番目のトランザクション検証は省かれがちですが、ここを確認しておくと「片方だけ実行されてデータが狂う」事故を本番前に潰せます。存在しないテーブル名にわざと変えるなどして、rollbackが効くことを一度見ておきましょう。
次の一手:ログイン認証・検索機能へ広げる
CRUDが書けるようになったら、次は「誰が・どのデータを操作できるか」を制御する段階です。ここで土台になるのが、今回のprepared statementと、ログイン認証の組み合わせです。
まずはPHPログイン機能を組み合わせ、「ログインしたユーザーだけが自分のデータをCRUDできる」仕組みにするのが定番の発展形です。さらに、一覧を絞り込む検索機能(LIKEによるあいまい検索)や、件数の多い一覧を分割するページネーションへと広げると、実用的な管理画面に近づきます。
また、登録や更新の完了時に通知メールを送りたくなったら、PHPMailerでのメール送信が役立ちます。いずれも、今回身につけた「prepared statementで安全に書く」という作法がそのまま生きます。
よくある質問
Q. PDOとmysqli、どちらを使うべきですか?
これから学ぶならPDOをおすすめします。対応データベースが幅広く、名前付きプレースホルダでコードが読みやすいためです。すでにmysqliで書かれた既存コードを触る場合はmysqliに合わせますが、新規ならPDOで統一すると移植性が高くなります。
Q. query()とprepare()はどう使い分けますか?
外部からの値(フォーム入力やURLパラメータ)を条件に含む場合は、必ずprepare()でprepared statementを使います。値を一切含まない固定のSQL(全件取得など)に限ってquery()を使えます。迷ったらprepare()を使っておけば安全です。
Q. UPDATEやDELETEで全件操作してしまいました。防ぐ方法は?
UPDATE・DELETEを書くときは、先にWHERE句を書いてから他を埋めるのが効果的です。また、開発用データベースで試してから本番に適用する、重要なテーブルは操作前にバックアップを取る、といった運用面の対策も併用すると安全です。
Q. トランザクションは毎回使うべきですか?
単独のINSERTやUPDATEには不要です。トランザクションが必要なのは「複数の変更がセットで、片方だけ成功すると整合性が崩れる」場面です。在庫と注文、振替の出金と入金など、まとめて成功・失敗させたい処理で使います。
Q. フレームワークのORMを使えば生のSQLは不要では?
実務ではLaravelのEloquentなどORMを使う場面も多いですが、内部で行われているのは結局CRUDとprepared statementです。生のPDOで仕組みを理解しておくと、ORMが生成するSQLを読めるようになり、パフォーマンス問題や複雑なクエリの場面で対応できます。土台として無駄になりません。
まとめ
PHPでMySQLを操作する基本は、PDOで接続し、CRUD(登録・取得・更新・削除)をprepared statementで安全に実行することに尽きます。外部からの値は必ずプレースホルダ経由で渡し、UPDATEとDELETEはWHEREで対象を限定し、複数の変更はトランザクションでまとめる——この3つを守れば、動的サイトのデータ操作の土台はほぼ固まります。
まずは本記事のitemsテーブルとCRUDコードをローカルで一度動かし、検証チェックリストで挙動を確かめてみてください。動いたら、ログイン認証や検索機能と組み合わせて、実用的な管理機能へ広げていけます。
CRUDを含むWebアプリの実装や、既存システムのデータベース周りの改修を相談したい場合は、Web制作・開発を手がけるRINIAにお問い合わせください。
