数据库交互
PHP 与数据库的交互是 Web 开发中最核心的功能之一。PHP 通过 PDO(PHP Data Objects)扩展提供了一个统一的数据库访问接口,支持多种数据库系统(MySQL、PostgreSQL、SQLite、SQL Server 等)。本节将全面介绍如何使用 PDO 连接数据库、执行查询、获取数据、防止 SQL 注入以及错误处理,并给出完整的 CRUD 示例。
前置知识
阅读本节前,你需要:
基础概念
什么是 PDO
PDO(PHP Data Objects)是 PHP 提供的数据库抽象层,它定义了一个统一的接口来访问多种数据库。PDO 的主要优势包括:
- 数据库无关性:使用相同的 API 访问不同的数据库,切换数据库只需修改连接字符串(DSN)
- 预处理语句:原生支持预处理语句,有效防止 SQL 注入攻击
- 面向对象接口:使用 OOP 风格的 API,更符合现代 PHP 的编码习惯
- 错误处理:支持异常模式,便于错误捕获和处理
- 高性能:使用本地驱动,性能优于 ADODB、MDB2 等抽象层
PDO 支持的数据库驱动
| 驱动名称 | 数据库 | DSN 示例 |
|---|---|---|
pdo_mysql | MySQL/MariaDB | mysql:host=localhost;dbname=test |
pdo_pgsql | PostgreSQL | pgsql:host=localhost;dbname=test |
pdo_sqlite | SQLite | sqlite:/path/to/database.sqlite |
pdo_sqlsrv | SQL Server | sqlsrv:Server=localhost;Database=test |
pdo_oci | Oracle | oci:dbname=localhost/XE |
检查可用的 PDO 驱动
使用 PDO::getAvailableDrivers() 可以查看当前 PHP 环境中可用的 PDO 驱动。本节所有示例使用 MySQL 数据库。
PDO 连接数据库
创建数据库连接
使用 PDO 连接数据库需要构建一个 DSN(Data Source Name)字符串,并指定用户名和密码:
<?php
declare(strict_types=1);
// database-connection.php — PDO 连接数据库示例
// 数据库配置
$dbConfig = [
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'php_tutorial',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
];
// 构建 DSN
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['port'],
$dbConfig['database'],
$dbConfig['charset']
);
// PDO 连接选项
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 禁用模拟预处理
PDO::ATTR_PERSISTENT => false, // 非持久连接(推荐)
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8mb4'", // 设置字符集
];
try {
$pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password'], $options);
echo "数据库连接成功\n";
echo "PDO 驱动版本: " . $pdo->getAttribute(PDO::ATTR_SERVER_VERSION) . "\n";
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage() . "\n");
}连接管理最佳实践
在实际项目中,推荐使用单例模式或依赖注入来管理数据库连接:
<?php
declare(strict_types=1);
/**
* 数据库连接管理类
* 使用单例模式确保全局只有一个数据库连接
*/
class Database
{
private static ?self $instance = null;
private ?PDO $connection = null;
private function __construct(
private readonly string $host = '127.0.0.1',
private readonly int $port = 3306,
private readonly string $database = 'php_tutorial',
private readonly string $username = 'root',
private readonly string $password = '',
private readonly string $charset = 'utf8mb4'
) {
$this->connect();
}
public static function getInstance(
string $host = '127.0.0.1',
int $port = 3306,
string $database = 'php_tutorial',
string $username = 'root',
string $password = ''
): self {
if (self::$instance === null) {
self::$instance = new self($host, $port, $database, $username, $password);
}
return self::$instance;
}
private function connect(): void
{
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$this->host,
$this->port,
$this->database,
$this->charset
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8mb4'",
];
$this->connection = new PDO($dsn, $this->username, $this->password, $options);
}
public function getConnection(): PDO
{
return $this->connection;
}
/**
* 执行查询并返回所有结果
*/
public function query(string $sql, array $params = []): array
{
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
/**
* 执行查询并返回单行结果
*/
public function queryOne(string $sql, array $params = []): ?array
{
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetch();
return $result ?: null;
}
/**
* 执行语句(INSERT/UPDATE/DELETE)
*/
public function execute(string $sql, array $params = []): int
{
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
}执行查询
使用 query() 直接执行
对于没有用户输入的简单查询,可以使用 query() 方法直接执行:
<?php
declare(strict_types=1);
// 创建示例表
$sql = "CREATE TABLE IF NOT EXISTS `users` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(100) NOT NULL UNIQUE,
`age` TINYINT UNSIGNED DEFAULT NULL,
`bio` TEXT DEFAULT NULL,
`status` ENUM('active', 'inactive', 'banned') NOT NULL DEFAULT 'active',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$pdo->exec($sql);
echo "表 users 创建成功\n";
// 使用 query() 执行 SELECT
$users = $pdo->query("SELECT * FROM users WHERE status = 'active' ORDER BY id DESC LIMIT 10");
foreach ($users as $user) {
echo "ID: {$user['id']}, 用户名: {$user['username']}, 邮箱: {$user['email']}\n";
}使用 prepare() 预处理语句
预处理语句是 PDO 最重要、最推荐的使用方式。它通过参数绑定的方式防止 SQL 注入:
<?php
declare(strict_types=1);
// ========== 预处理语句 — 位置参数 ==========
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND status = ?");
$stmt->execute([1, 'active']);
$user = $stmt->fetch();
// ? 占位符按位置绑定
// ========== 预处理语句 — 命名参数(推荐) ==========
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id AND status = :status");
$stmt->execute([
':id' => 1,
':status' => 'active',
]);
$user = $stmt->fetch();
// ========== bindValue vs bindParam ==========
// bindValue — 绑定值(推荐,更安全)
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->bindValue(':email', 'zhangsan@example.com', PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch();
// bindParam — 绑定引用(用于存储过程和特殊场景)
$stmt = $pdo->prepare("SELECT COUNT(*) as total FROM users WHERE age > :minAge");
$minAge = 18;
$stmt->bindParam(':minAge', $minAge, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetch();
echo "18 岁以上用户数: {$result['total']}\n";
// 注意:bindParam 绑定的是变量引用,不是变量的值
// 如果在 execute() 之前改变了变量的值,使用的是新值禁用模拟预处理
始终设置 PDO::ATTR_EMULATE_PREPARES => false,确保预处理语句在服务器端执行,而不是在 PHP 端模拟。这是防止 SQL 注入的重要防线。
获取数据
fetch() 获取单行数据
fetch() 方法每次从结果集中获取一行数据,支持多种获取模式:
<?php
declare(strict_types=1);
// 获取模式设置
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// PDO::FETCH_ASSOC — 关联数组(最常用)
$stmt = $pdo->query("SELECT id, username, email FROM users LIMIT 1");
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 结果:['id' => 1, 'username' => '张三', 'email' => 'zhangsan@example.com']
// PDO::FETCH_NUM — 数字索引数组
$user = $stmt->fetch(PDO::FETCH_NUM);
// 结果:[0 => 1, 1 => '张三', 2 => 'zhangsan@example.com']
// PDO::FETCH_BOTH — 同时包含关联和数字索引(默认)
$user = $stmt->fetch(PDO::FETCH_BOTH);
// PDO::FETCH_OBJ — 匿名对象
$user = $stmt->fetch(PDO::FETCH_OBJ);
// 结果:object { id: 1, username: '张三', email: 'zhangsan@example.com' }
echo "用户名: " . $user->username . "\n";
// PDO::FETCH_CLASS — 映射到指定类
class UserDTO
{
public function __construct(
public readonly ?int $id = null,
public readonly ?string $username = null,
public readonly ?string $email = null,
public readonly ?string $status = null,
) {
}
}
$stmt = $pdo->query("SELECT id, username, email, status FROM users LIMIT 1");
$user = $stmt->fetch(PDO::FETCH_CLASS, UserDTO::class);
echo "用户: {$user->username} ({$user->email})\n";
// 使用 fetchAll() 一次性获取所有数据
$stmt = $pdo->query("SELECT id, username, email FROM users WHERE status = 'active'");
$activeUsers = $stmt->fetchAll(PDO::FETCH_CLASS, UserDTO::class);
foreach ($activeUsers as $user) {
echo " - {$user->username} ({$user->email})\n";
}
// 使用 fetchColumn() 获取单个列的值
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
$totalUsers = $stmt->fetchColumn();
echo "用户总数: {$totalUsers}\n";插入、更新、删除数据
INSERT 操作
<?php
declare(strict_types=1);
// 插入单条数据
$sql = "INSERT INTO users (username, email, age, bio, status) VALUES (:username, :email, :age, :bio, :status)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':username' => 'zhangsan',
':email' => 'zhangsan@example.com',
':age' => 28,
':bio' => 'PHP 开发者,热爱编程。',
':status' => 'active',
]);
// 获取最后插入的 ID
$lastInsertId = $pdo->lastInsertId();
echo "新用户 ID: {$lastInsertId}\n";
// 使用 exec() 快速执行(仅用于没有参数的场景)
$count = $pdo->exec("UPDATE users SET status = 'active' WHERE status = 'inactive'");
echo "更新了 {$count} 行\n";UPDATE 操作
<?php
declare(strict_types=1);
// 更新用户信息
$sql = "UPDATE users SET username = :username, email = :email, age = :age WHERE id = :id";
$stmt = $pdo->prepare($sql);
$affectedRows = $stmt->execute([
':username' => 'zhangsan_updated',
':email' => 'zhangsan_new@example.com',
':age' => 29,
':id' => 1,
]);
echo "更新了 {$affectedRows} 行\n";
// 批量更新
$updates = [
['id' => 1, 'status' => 'active'],
['id' => 2, 'status' => 'inactive'],
['id' => 3, 'status' => 'active'],
];
$sql = "UPDATE users SET status = :status WHERE id = :id";
$stmt = $pdo->prepare($sql);
$totalUpdated = 0;
foreach ($updates as $update) {
$stmt->execute($update);
$totalUpdated += $stmt->rowCount();
}
echo "批量更新了 {$totalUpdated} 行\n";DELETE 操作
<?php
declare(strict_types=1);
// 删除单条记录
$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => 1]);
echo "删除了 {$stmt->rowCount()} 行\n";
// 带条件的删除(防止误删)
$sql = "DELETE FROM users WHERE status = :status AND created_at < :date";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':status' => 'banned',
':date' => '2023-01-01 00:00:00',
]);
echo "删除了 {$stmt->rowCount()} 个已封禁的旧账户\n";预处理语句防 SQL 注入
SQL 注入的原理
SQL 注入是指攻击者通过在用户输入中嵌入恶意 SQL 代码来操控数据库查询。预处理语句通过将 SQL 语句结构和数据分开处理,从根本上防止了 SQL 注入:
<?php
declare(strict_types=1);
/**
* SQL 注入防护演示
*/
// ===== 危险做法 — 绝对不要这样做 =====
$userId = "1 OR 1=1"; // 恶意输入
// $sql = "SELECT * FROM users WHERE id = {$userId}";
// 这会变成:SELECT * FROM users WHERE id = 1 OR 1=1
// 攻击者可以获取所有用户数据!
$userId = "1; DROP TABLE users; --";
// $sql = "SELECT * FROM users WHERE id = {$userId}";
// 这会变成:SELECT * FROM users WHERE id = 1; DROP TABLE users; --
// 攻击者可以删除整个表!
// ===== 安全做法 — 使用预处理语句 =====
$userId = "1 OR 1=1"; // 即使是恶意输入
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $userId]);
// 实际执行的 SQL:SELECT * FROM users WHERE id = '1 OR 1=1'
// PDO 会将整个字符串作为普通参数值,不会解析为 SQL 代码
// 查询结果:空(因为不存在 id 为 "1 OR 1=1" 的记录)
// ===== 动态表名/列名的问题 =====
// 注意:预处理语句不能用于表名和列名(因为它们不是值)
// 如果需要动态表名,必须手动验证白名单:
function queryTable(PDO $pdo, string $tableName, int $limit = 10): array
{
// 白名单验证表名
$allowedTables = ['users', 'products', 'orders', 'categories'];
if (!in_array($tableName, $allowedTables, true)) {
throw new \InvalidArgumentException("不允许的表名: {$tableName}");
}
$limit = max(1, min(100, $limit)); // 验证限制值
$stmt = $pdo->prepare("SELECT * FROM `{$tableName}` LIMIT :limit");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
echo "安全查询机制演示完成\n";错误处理
PDO 错误模式
PDO 提供了三种错误处理模式:
| 错误模式 | 常量 | 说明 |
|---|---|---|
| 静默模式 | PDO::ERRMODE_SILENT | 默认模式,只设置错误代码,不抛出异常 |
| 警告模式 | PDO::ERRMODE_WARNING | 触发 PHP Warning |
| 异常模式 | PDO::ERRMODE_EXCEPTION | 抛出 PDOException(推荐) |
<?php
declare(strict_types=1);
/**
* 错误处理最佳实践
*/
class DatabaseService
{
public function __construct(
private readonly PDO $pdo
) {
}
/**
* 在事务中执行操作 — 完整的错误处理
*/
public function transferUserStatus(int $fromUserId, int $toUserId): string
{
try {
// 开启事务
$this->pdo->beginTransaction();
// 1. 检查源用户是否存在
$stmt = $this->pdo->prepare("SELECT id, username, status FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $fromUserId]);
$fromUser = $stmt->fetch();
if (!$fromUser) {
throw new RuntimeException("源用户不存在: ID {$fromUserId}");
}
// 2. 检查目标用户是否存在
$stmt = $this->pdo->prepare("SELECT id, username FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $toUserId]);
$toUser = $stmt->fetch();
if (!$toUser) {
throw new RuntimeException("目标用户不存在: ID {$toUserId}");
}
// 3. 更新源用户状态
$stmt = $this->pdo->prepare("UPDATE users SET status = 'inactive' WHERE id = :id");
$stmt->execute([':id' => $fromUserId]);
// 4. 更新目标用户状态
$stmt = $this->pdo->prepare("UPDATE users SET status = :status WHERE id = :id");
$stmt->execute([':id' => $toUserId, ':status' => $fromUser['status']]);
// 提交事务
$this->pdo->commit();
return "成功将用户 {$fromUser['username']} 的状态转移给 {$toUser['username']}";
} catch (PDOException $e) {
// 回滚事务
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
// 记录错误日志
error_log("数据库错误: " . $e->getMessage());
throw new RuntimeException("数据库操作失败: " . $e->getMessage());
} catch (RuntimeException $e) {
// 回滚事务
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
throw $e;
}
}
}实战示例
完整的 CRUD 示例
<?php
declare(strict_types=1);
/**
* 完整的用户管理 CRUD 类
* 展示 PDO 在实际项目中的使用方式
*/
class UserRepository
{
public function __construct(
private readonly PDO $pdo
) {
}
// ========== CREATE ==========
public function create(array $data): int
{
$sql = "INSERT INTO users (username, email, age, bio, status)
VALUES (:username, :email, :age, :bio, :status)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':username' => $data['username'],
':email' => $data['email'],
':age' => $data['age'] ?? null,
':bio' => $data['bio'] ?? null,
':status' => $data['status'] ?? 'active',
]);
return (int) $this->pdo->lastInsertId();
}
// ========== READ ==========
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
$result = $stmt->fetch();
return $result ?: null;
}
public function findByEmail(string $email): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = :email LIMIT 1");
$stmt->execute([':email' => $email]);
$result = $stmt->fetch();
return $result ?: null;
}
public function findAll(int $page = 1, int $perPage = 20, string $status = ''): array
{
$offset = ($page - 1) * $perPage;
$params = [];
$where = '';
if ($status !== '') {
$where = "WHERE status = :status";
$params[':status'] = $status;
}
// 获取分页数据
$sql = "SELECT id, username, email, age, status, created_at FROM users {$where}
ORDER BY id DESC LIMIT :limit OFFSET :offset";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$items = $stmt->fetchAll();
// 获取总数
$countSql = "SELECT COUNT(*) FROM users {$where}";
$countStmt = $this->pdo->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$total = (int) $countStmt->fetchColumn();
return [
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => (int) ceil($total / $perPage),
];
}
public function search(string $keyword, int $limit = 10): array
{
$like = "%{$keyword}%";
$stmt = $this->pdo->prepare(
"SELECT id, username, email FROM users
WHERE username LIKE :keyword OR email LIKE :keyword
ORDER BY id DESC LIMIT :limit"
);
$stmt->bindValue(':keyword', $like);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
// ========== UPDATE ==========
public function update(int $id, array $data): bool
{
$allowedFields = ['username', 'email', 'age', 'bio', 'status'];
$setClauses = [];
$params = [':id' => $id];
foreach ($data as $field => $value) {
if (in_array($field, $allowedFields, true)) {
$setClauses[] = "`{$field}` = :{$field}";
$params[":{$field}"] = $value;
}
}
if (empty($setClauses)) {
return false;
}
$sql = "UPDATE users SET " . implode(', ', $setClauses) . " WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount() > 0;
}
// ========== DELETE ==========
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare("DELETE FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
// ========== 统计 ==========
public function countByStatus(): array
{
$stmt = $this->pdo->query(
"SELECT status, COUNT(*) as count FROM users GROUP BY status"
);
$result = [];
foreach ($stmt->fetchAll() as $row) {
$result[$row['status']] = (int) $row['count'];
}
return $result;
}
}
// ===== 使用示例 =====
// $pdo = Database::getInstance()->getConnection();
// $repo = new UserRepository($pdo);
// 创建用户
// $id = $repo->create([
// 'username' => 'wangwu',
// 'email' => 'wangwu@example.com',
// 'age' => 32,
// 'bio' => '全栈开发者',
// ]);
// echo "创建用户 ID: {$id}\n";
// 查询用户
// $user = $repo->findById($id);
// print_r($user);
// 更新用户
// $repo->update($id, ['age' => 33, 'bio' => '资深全栈开发者']);
// 搜索用户
// $results = $repo->search('wang', 5);
// print_r($results);
// 分页查询
// $page = $repo->findAll(page: 1, perPage: 10, status: 'active');
// echo "共 {$page['total']} 条,第 {$page['page']}/{$page['total_pages']} 页\n";
// 删除用户
// $repo->delete($id);
echo "CRUD 操作演示完成\n";初始化数据库的脚本
<?php
declare(strict_types=1);
/**
* 数据库初始化脚本
* 创建表并插入示例数据
*/
class DatabaseInitializer
{
public function __construct(
private readonly PDO $pdo
) {
}
public function initialize(): void
{
$this->createUsersTable();
$this->createArticlesTable();
$this->insertSampleData();
echo "数据库初始化完成\n";
}
private function createUsersTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS `users` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(100) NOT NULL UNIQUE,
`age` TINYINT UNSIGNED DEFAULT NULL,
`bio` TEXT DEFAULT NULL,
`status` ENUM('active', 'inactive', 'banned') NOT NULL DEFAULT 'active',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->pdo->exec($sql);
}
private function createArticlesTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS `articles` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED NOT NULL,
`title` VARCHAR(200) NOT NULL,
`content` TEXT NOT NULL,
`status` ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->pdo->exec($sql);
}
private function insertSampleData(): void
{
// 检查是否已有数据
$count = (int) $this->pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
if ($count > 0) {
echo "数据已存在,跳过初始化\n";
return;
}
// 插入示例用户
$stmt = $this->pdo->prepare("INSERT INTO users (username, email, age, bio, status) VALUES (?, ?, ?, ?, ?)");
$users = [
['zhangsan', 'zhangsan@example.com', 28, 'PHP 开发者', 'active'],
['lisi', 'lisi@example.com', 32, '全栈工程师', 'active'],
['wangwu', 'wangwu@example.com', 25, '前端开发者', 'inactive'],
['zhaoliu', 'zhaoliu@example.com', 35, '架构师', 'active'],
['tianqi', 'tianqi@example.com', 22, '实习开发者', 'active'],
];
foreach ($users as $user) {
$stmt->execute($user);
}
// 插入示例文章
$stmt = $this->pdo->prepare("INSERT INTO articles (user_id, title, content, status) VALUES (?, ?, ?, ?)");
$articles = [
[1, 'PHP 入门教程', '这是一篇关于 PHP 入门的教程...', 'published'],
[1, 'MySQL 基础', '学习 MySQL 数据库的基础知识...', 'published'],
[2, 'Docker 容器化部署', '使用 Docker 部署 PHP 应用...', 'draft'],
[3, 'Vue.js 实战', 'Vue.js 前端框架的实战教程...', 'published'],
[4, '系统架构设计', '大型系统的架构设计思路...', 'draft'],
];
foreach ($articles as $article) {
$stmt->execute($article);
}
echo "已插入 5 个用户和 5 篇文章\n";
}
}
// CLI 模式下的演示
if (PHP_SAPI === 'cli') {
echo "=== 数据库初始化脚本 ===\n";
echo "在实际环境中,请确保 MySQL/MariaDB 服务已启动。\n";
echo "创建数据库: CREATE DATABASE php_tutorial CHARACTER SET utf8mb4;\n";
}注意事项
- 始终使用预处理语句:永远不要将用户输入直接拼接到 SQL 语句中,无论输入来源如何。预处理语句是防止 SQL 注入的最有效方式。
- 连接安全:数据库凭证不应硬编码在源代码中。使用环境变量或配置文件来存储敏感信息。
- 字符集:始终使用
utf8mb4字符集(而不是utf8),以支持完整的 Unicode 字符(包括 Emoji)。 - 持久连接:不建议在普通 Web 应用中使用持久连接(
PDO::ATTR_PERSISTENT => true),它可能导致连接耗尽等问题。 - 事务使用:确保在 try/catch 中使用事务,并在异常时回滚。不要在事务中执行耗时操作。
- NULL 处理:注意
null和空字符串''的区别。PDO 对null值会正确处理为 SQL 的NULL。 - LIMIT 参数:使用预处理语句时,LIMIT 和 OFFSET 参数必须使用
bindValue()和PDO::PARAM_INT类型声明。
最佳实践
- 使用 DSN 配置数据库连接,便于切换数据库类型。
- 始终设置
PDO::ERRMODE_EXCEPTION为错误处理模式。 - 始终设置
PDO::ATTR_EMULATE_PREPARES => false禁用模拟预处理。 - 使用
PDO::FETCH_ASSOC作为默认获取模式。 - 使用 Repository 模式封装数据库操作,将 SQL 逻辑集中在数据访问层。
- 复杂查询使用事务(BEGIN/COMMIT/ROLLBACK)确保数据一致性。
- 在生产环境中使用连接池(如 Swoole Database Proxy)管理高并发连接。
- 使用
password_hash()和password_verify()处理密码存储,而不是明文或 MD5。 - 为查询添加适当的索引,提高查询性能。
- 避免在循环中执行单条 SQL,使用批量操作(批量 INSERT/UPDATE)提高效率。
下一节
掌握了数据库交互之后,你已经具备了构建完整 Web 应用的基础能力。接下来可以继续学习 PHP 的面向对象编程、Composer 包管理和框架使用等进阶主题。