PHP MySQL 数据库

PHP 与 MySQL 是经典组合,通过 MySQLi 或 PDO 扩展进行数据库操作。MySQLi(MySQL Improved)是专为 MySQL 设计的扩展,提供了面向对象和过程式两种 API,支持预处理语句、事务、多语句执行等特性。本文重点介绍面向对象方式,这也是现代 PHP 项目的推荐做法。

连接数据库(MySQLi)

建立数据库连接是所有数据库操作的第一步。MySQLi 的面向对象方式通过 new mysqli() 创建连接对象,连接后应立即设置字符集为 utf8mb4,以支持完整的 Unicode 字符(包括 emoji)。

PHP 实例
$host   = 'localhost';
$user   = 'root';
$pass   = 'your_password';
$dbname = 'myapp';

$conn = new mysqli($host, $user, $pass, $dbname); $conn->set_charset('utf8mb4');

if ($conn->connect_error) { die("连接失败:" . $conn->connect_error); }

echo "连接成功"; ``

生产环境中,数据库连接信息不应硬编码在代码中,应通过环境变量或配置文件读取。推荐使用 vlucas/phpdotenv 库读取 .env 文件,并将 .env 加入 .gitignore,防止敏感信息泄露到版本控制系统。

创建数据表

在操作数据之前,需要先设计并创建数据表。良好的表结构设计是数据库性能和数据完整性的基础。

`sql CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, password VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `

password 字段长度设为 255 是因为 password_hash() 使用 bcrypt 算法生成的哈希值长度约为 60 字符,但未来可能使用更长的算法,预留足够空间是好习惯。

增删改查(CRUD)

插入数据

永远不要用字符串拼接构造 SQL 语句,这会导致 SQL 注入漏洞。预处理语句(Prepared Statements)是防止 SQL 注入的标准方法,参数通过 bind_param() 绑定,与 SQL 语句分离传输。

`php $stmt = $conn->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)"); $stmt->bind_param("sss", $username, $email, $hashed);

$username = "张三"; $email = "zhangsan@example.com"; $hashed = password_hash("123456", PASSWORD_DEFAULT);

$stmt->execute(); echo "插入成功,ID:" . $conn->insert_id; $stmt->close(); `

查询数据

查询是最常见的数据库操作。对于不带用户输入的简单查询,可以直接用 query() 方法;带参数的查询必须使用预处理语句。

`php // 查询所有 $result = $conn->query("SELECT id, username, email FROM users"); while ($row = $result->fetch_assoc()) { echo "{$row['id']} - {$row['username']} - {$row['email']}\n"; }

// 带条件查询(预处理) $stmt = $conn->prepare("SELECT * FROM users WHERE id = ?"); $stmt->bind_param("i", $id); $id = 1; $stmt->execute(); $user = $stmt->get_result()->fetch_assoc(); echo $user['username']; $stmt->close(); `

更新数据

`php $stmt = $conn->prepare("UPDATE users SET email = ? WHERE id = ?"); $stmt->bind_param("si", $email, $id);

$email = "new@example.com"; $id = 1; $stmt->execute();

echo "影响行数:" . $stmt->affected_rows; $stmt->close(); `

删除数据

`php $stmt = $conn->prepare("DELETE FROM users WHERE id = ?"); $stmt->bind_param("i", $id); $id = 1; $stmt->execute(); echo "删除成功"; $stmt->close(); `

执行删除操作前,建议先查询确认记录存在,并在业务层做权限验证,防止越权删除。对于重要数据,可以考虑软删除(添加 deleted_at 字段)而不是物理删除。

bind_param 类型说明

字符类型
sstring 字符串
iinteger 整数
ddouble 浮点数
bblob 二进制

事务处理

事务保证了一组数据库操作的原子性——要么全部成功,要么全部回滚。这在涉及多表操作的业务场景(如转账、下单)中至关重要。

`php $conn->begin_transaction();

try { $stmt = $conn->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?"); $stmt->bind_param("di", $amount, $fromId); $amount = 500; $fromId = 1; $stmt->execute();

$stmt = $conn->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?"); $stmt->bind_param("di", $amount, $toId); $toId = 2; $stmt->execute();

$conn->commit(); echo "转账成功"; } catch (Exception $e) { $conn->rollback(); echo "转账失败:" . $e->getMessage(); } `

分页查询

分页是列表页面的标配功能,通过 LIMITOFFSET 实现。需要同时查询总记录数来计算总页数。

`php $page = max(1, (int)($_GET['page'] ?? 1)); $pageSize = 10; $offset = ($page - 1) * $pageSize;

$total = $conn->query("SELECT COUNT(*) FROM users")->fetch_row()[0]; $pages = ceil($total / $pageSize);

$stmt = $conn->prepare("SELECT * FROM users ORDER BY id DESC LIMIT ? OFFSET ?"); $stmt->bind_param("ii", $pageSize, $offset); $stmt->execute(); $users = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);

foreach ($users as $user) { echo $user['username'] . "\n"; }

echo "第 $page / $pages 页,共 $total 条"; `

关闭连接

`php $conn->close();

PHP 脚本结束时会自动关闭数据库连接,但在长时间运行的脚本(如 CLI 任务)中,应在不再需要连接时主动关闭,释放数据库服务器的连接资源。

常见问题

Q:MySQLi 和 PDO 应该选哪个? A:对于只使用 MySQL 的项目,两者都可以。PDO 的优势在于支持多种数据库,命名参数(:name)比 MySQLi 的 ? 占位符更清晰,异常处理也更统一。MySQLi 的优势是对 MySQL 特性支持更完整。新项目推荐使用 PDO,尤其是可能需要切换数据库的场景。

Q:预处理语句真的能完全防止 SQL 注入吗? A:正确使用预处理语句可以防止 SQL 注入,因为参数值与 SQL 语句分离传输。但有几点注意:表名和列名不能用预处理语句参数化,如果需要动态表名/列名,必须用白名单验证;预处理语句不能防止业务逻辑漏洞(如越权访问)。

Q:如何处理 MySQLi 的错误,避免程序崩溃? A:推荐在连接时开启异常模式:mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT),这样 MySQLi 操作失败时会自动抛出 mysqli_sql_exception,可以用 try/catch 统一处理。

Q:数据库连接应该在哪里创建,如何避免重复连接? A:推荐使用单例模式或依赖注入容器管理数据库连接,确保整个请求生命周期内只创建一个连接。最简单的方式是用静态变量:static $conn = null; if (!$conn) { $conn = new mysqli(...); }`。