面向对象风格,并重点讲解预处理语句在增、删、改、查四种操作中的标准流程。预处理语句能有效防御SQL注入,并提升重复执行时的性能。
一、为什么使用MySQLi预处理?
优势 说明 安全性 数据和SQL命令分离,恶意输入无法改变查询结构 性能 数据库预编译SQL模板,多次执行只需发送参数 便捷性 自动处理数据类型和转义,无需手动 real_escape_string
二、核心执行流程(四步法)
连接数据库(创建
mysqli对象)准备SQL模板(使用
prepare(),占位符用?)绑定参数(
bind_param()指定类型和变量)执行语句(
execute())处理结果集(SELECT语句需要
get_result()或bind_result())
三、连接数据库与异常处理
MySQLi默认不抛出异常,建议开启错误报告模式:
<?php
// 开启 mysqli 异常模式(推荐)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$host = 'localhost';
$user = 'root';
$pass = '123456';
$dbname = 'testdb';
$port = 3306;
try {
$mysqli = new mysqli($host, $user, $pass, $dbname, $port);
$mysqli->set_charset('utf8mb4'); // 解决中文乱码
echo "连接成功<br>";
} catch (mysqli_sql_exception $e) {
die("连接失败:" . $e->getMessage());
}
四、增(INSERT)—— 插入数据
// SQL模板,使用问号占位符
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
// 准备语句
$stmt = $mysqli->prepare($sql);
// 绑定参数:第一个参数是类型字符串,s=string, i=integer, d=double, b=blob
$username = '王小明';
$email = 'wang@example.com';
$age = 25;
$stmt->bind_param('ssi', $username, $email, $age); // 三个占位符类型分别为 s s i
// 执行
$stmt->execute();
// 获取插入ID和影响行数
echo "新记录ID:" . $stmt->insert_id . "<br>";
echo "影响行数:" . $stmt->affected_rows . "<br>";
// 关闭语句(可选,脚本结束自动释放)
$stmt->close();
批量插入示例(重复执行同一模板):
$stmt = $mysqli->prepare("INSERT INTO logs (message, level) VALUES (?, ?)");
$stmt->bind_param('ss', $msg, $lvl);
$logs = [
['用户登录', 'INFO'],
['密码错误', 'WARNING'],
['数据库异常', 'ERROR']
];
foreach ($logs as $log) {
$msg = $log[0];
$lvl = $log[1];
$stmt->execute();
}
echo "批量插入完成,共执行 " . $stmt->affected_rows . " 条(实际每次覆盖)";
五、删(DELETE)—— 删除数据
$sql = "DELETE FROM users WHERE id = ?";
$stmt = $mysqli->prepare($sql);
$id = 10;
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
echo "成功删除 {$stmt->affected_rows} 条记录";
} else {
echo "未找到id为 $id 的用户";
}
}
$stmt->close();
六、改(UPDATE)—— 更新数据
$sql = "UPDATE users SET email = ?, age = ? WHERE username = ?";
$stmt = $mysqli->prepare($sql);
$newEmail = 'newemail@domain.com';
$newAge = 26;
$username = '王小明';
$stmt->bind_param('sis', $newEmail, $newAge, $username);
$stmt->execute();
echo "更新了 " . $stmt->affected_rows . " 条记录";
七、查(SELECT)—— 获取数据
SELECT查询有两种方式处理结果集:
方式一:get_result() + 普通取数据(推荐,简单灵活)
$sql = "SELECT id, username, email FROM users WHERE age > ?";
$stmt = $mysqli->prepare($sql);
$minAge = 20;
$stmt->bind_param('i', $minAge);
$stmt->execute();
// 获取结果集对象
$result = $stmt->get_result();
// 循环获取所有行
while ($row = $result->fetch_assoc()) {
echo "ID: {$row['id']}, 用户名: {$row['username']}, 邮箱: {$row['email']}<br>";
}
// 或者获取全部
// $rows = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
方式二:bind_result() + fetch()(内存更优,但列必须匹配)
$sql = "SELECT id, username, email FROM users WHERE id = ?";
$stmt = $mysqli->prepare($sql);
$id = 5;
$stmt->bind_param('i', $id);
$stmt->execute();
// 绑定结果变量
$stmt->bind_result($colId, $colUsername, $colEmail);
if ($stmt->fetch()) {
echo "ID: $colId, 用户名: $colUsername, 邮箱: $colEmail";
} else {
echo "未找到用户";
}
$stmt->close();
获取单行单列值:
$sql = "SELECT COUNT(*) FROM users";
$stmt = $mysqli->prepare($sql);
$stmt->execute();
$stmt->bind_result($count);
$stmt->fetch();
echo "用户总数:$count";
八、完整实战:用户登录与分页查询
登录验证(防SQL注入)
// 假设 $_POST['username'] 和 $_POST['password'] 来自表单 $loginUser = $_POST['username'] ?? ''; $loginPass = $_POST['password'] ?? ''; $sql = "SELECT id, username, password_hash FROM users WHERE username = ?"; $stmt = $mysqli->prepare($sql); $stmt->bind_param('s', $loginUser); $stmt->execute(); $result = $stmt->get_result(); $user = $result->fetch_assoc(); if ($user && password_verify($loginPass, $user['password_hash'])) { echo "登录成功,欢迎 " . htmlspecialchars($user['username']); } else { echo "用户名或密码错误"; }分页查询(安全传递limit参数)
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $perPage = 10; $offset = ($page - 1) * $perPage; $sql = "SELECT id, username, email FROM users LIMIT ?, ?"; $stmt = $mysqli->prepare($sql); $stmt->bind_param('ii', $offset, $perPage); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { echo "{$row['username']} - {$row['email']}<br>"; }九、错误处理(捕获异常)
由于我们开启了mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT),所有MySQLi错误都会自动抛出异常,直接使用try-catch即可:
try { $stmt = $mysqli->prepare("INSERT INTO users (username, email) VALUES (?, ?)"); $stmt->bind_param('ss', $name, $mail); $name = "李四"; $mail = "lisi@test.com"; $stmt->execute(); } catch (mysqli_sql_exception $e) { // 根据错误码处理 if ($mysqli->errno === 1062) { // 重复键错误 echo "该邮箱已被注册"; } else { echo "数据库错误:" . $e->getMessage(); } }如果不使用异常模式,也可以手动检查:
if (!$stmt->execute()) { echo "执行失败:" . $stmt->error; }十、重复执行优化(准备一次,执行多次)
预处理语句的最大性能优势体现在循环执行相同SQL结构时:
// 准备一次 $stmt = $mysqli->prepare("UPDATE products SET stock = stock - ? WHERE id = ?"); $stmt->bind_param('ii', $quantity, $productId); // 模拟批量扣减库存 $orders = [ ['product_id' => 101, 'qty' => 2], ['product_id' => 102, 'qty' => 1], ['product_id' => 103, 'qty' => 5] ]; foreach ($orders as $order) { $quantity = $order['qty']; $productId = $order['product_id']; $stmt->execute(); echo "影响了 " . $stmt->affected_rows . " 行<br>"; } $stmt->close();十一、常见误区与注意事项
误区 正确做法 表名/列名用占位符 ?占位符只能代替值,表名列名必须直接写在SQL中(需白名单验证) 忘记 bind_param的类型字符串必须根据占位符数量提供等长的类型字符串,如 'iss'使用 bind_result前未execute()顺序:prepare → bind_param → execute → bind_result → fetch 重复执行时重复 prepare应重用 $stmt对象,只需重新bind_param并execute获取结果集时混淆 fetch()和fetch_assoc()get_result()后用fetch_assoc();bind_result后用fetch()
动态表名/列名解决方案(需白名单)
$allowedColumns = ['id', 'username', 'email']; $sort = $_GET['sort'] ?? 'id'; if (!in_array($sort, $allowedColumns)) { $sort = 'id'; } $sql = "SELECT * FROM users ORDER BY $sort"; // 此处安全,因$sort已过滤十二、MySQLi与PDO预处理对比
特性 MySQLi PDO 数据库支持 仅MySQL 12+种数据库 命名占位符 ❌ 只支持 ?✅ 支持 :name面向对象 ✅ ✅ 异常处理 需手动开启 默认异常 存储过程支持 ✅ 较好 一般
结论:如果只用MySQL,MySQLi是完全够用的选择;如果需要切换数据库或偏爱命名占位符,选择PDO。
总结:CRUD流程速查表
操作 核心代码片段 连接 $mysqli = new mysqli(...);插入 $stmt = $mysqli->prepare("INSERT ..."); $stmt->bind_param("s", $val); $stmt->execute();查询 $stmt->execute(); $result = $stmt->get_result(); while($row=$result->fetch_assoc()){...}更新/删除 同插入,通过 $stmt->affected_rows获取影响行数关闭 $stmt->close(); $mysqli->close();
黄金法则:任何用户提供的数据在SQL中出现,都必须使用?占位符并通过bind_param传递。 坚守此原则,你的应用将免疫SQL注入攻击。
本教程基于PHP 7.4+及MySQLi面向对象风格,所有示例均已测试通过。
