PHPでMySQLを操作する方法|PDOでCRUD(登録・取得・更新・削除)を実装する


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つ、PDOmysqliがあります。本記事ではPDOを使います。理由は、対応データベースが幅広く(将来PostgreSQLなどへ移植しやすい)、名前付きプレースホルダが使えてコードが読みやすいためです。PDO全体の仕様はPHP公式のPDOマニュアルにまとまっており、迷ったときの一次情報として手元に置いておくと安心です。

観点PDOmysqli
対応DBMySQL・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
);

idAUTO_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.phpEMULATE_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.phpERRMODE_EXCEPTIONを設定しているからこそ、失敗をcatchで受け取れる点も重要です。


文字化け・つまずき対策

CRUD実装でハマりやすいポイントを先回りでまとめます。多くは接続設定とWHERE忘れに集約されます。

症状主な原因対処
日本語が「???」になる接続のcharset未設定DSNにcharset=utf8mb4/テーブルもutf8mb4で作成
更新・削除が全行に及んだWHEREの書き忘れUPDATE/DELETEは必ずWHEREで対象を限定
エラーが出ず原因不明ERRMODEが例外でないERRMODE_EXCEPTIONを設定して例外で気づく
数値がおかしい・型が不安定入力値を未整形で使用数値は(int)でキャストしてから使う

特にWHERE忘れは、開発中に一度はやりがちな事故です。UPDATEとDELETEを書くときは「WHEREを先に書く」癖をつけると安全です。


動作確認:実際に動かして検証する

コードを書いたら「書いた」で終わらせず、実際に動かして想定通りかを確認します。ローカル環境で、CRUDをひと通り通してチェックしてください。

  1. Create:登録後、phpMyAdminなどでitemsテーブルに行が増えているか
  2. Read(一覧):登録した商品が一覧に表示されるか
  3. Read(1件):?id=1のように指定して、該当商品だけ取得できるか。存在しないIDで「見つかりません」になるか
  4. Update:価格や在庫が変わり、rowCount()が更新件数を返すか
  5. Delete:対象だけが消え、他の行が残っているか
  6. トランザクション:わざと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にお問い合わせください。