PHP MySQL 预处理语句

预处理语句是一种数据库操作技术,它可以显著提高数据库操作的安全性和性能。本教程将介绍预处理语句的概念、优势以及如何在PHP中使用MySQLi和PDO实现预处理语句。

什么是预处理语句?

预处理语句(Prepared Statements)是一种数据库操作机制,它将SQL查询模板与实际参数分离。预处理语句首先发送SQL模板到数据库服务器进行编译,然后再传入实际参数执行。

为什么需要预处理语句?

预处理语句提供了以下主要优势:

  • 防止SQL注入攻击:这是预处理语句最重要的优势,它可以安全地处理用户输入的数据
  • 提高性能:同一个SQL模板只需编译一次,可多次执行
  • 代码清晰度:将SQL逻辑与数据分离,使代码更易于阅读和维护
  • 类型安全:可以指定参数的数据类型

安全警告:

在处理用户输入数据时,始终使用预处理语句或适当的转义函数来防止SQL注入攻击。不要直接将用户输入拼接到SQL语句中!

SQL注入攻击示例

以下是一个不使用预处理语句可能导致SQL注入的示例:

<?php
// 不安全的代码 - 不要这样做!
$username = $_POST['username'];
$password = $_POST['password'];

// 假设一个恶意用户输入:username='admin' OR '1'='1' --
$sql = "SELECT * FROM users WHERE username='$username' AND password='$password'";

// 生成的SQL将变成:
// SELECT * FROM users WHERE username='admin' OR '1'='1' --' AND password='输入的密码'
// 这个查询将返回所有用户记录,因为 '1'='1' 始终为真,-- 会注释掉后面的条件
?>

使用MySQLi预处理语句

MySQLi扩展提供了面向对象和过程式两种方式来使用预处理语句。

1. MySQLi面向对象方式

<?php
// 数据库连接参数
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";

// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);

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

// 准备预处理语句
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");

// 绑定参数
$stmt->bind_param("sss", $firstname, $lastname, $email);

// 设置参数并执行
$firstname = "John";
$lastname = "Doe";
$email = "john@example.com";
$stmt->execute();

// 可以使用相同的预处理语句插入其他记录
$firstname = "Mary";
$lastname = "Moe";
$email = "mary@example.com";
$stmt->execute();

$firstname = "Julie";
$lastname = "Dooley";
$email = "julie@example.com";
$stmt->execute();

echo "新记录插入成功";

// 关闭语句和连接
$stmt->close();
$conn->close();
?>

2. MySQLi过程式方式

<?php
// 数据库连接参数
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";

// 创建连接
$conn = mysqli_connect($servername, $username, $password, $dbname);

// 检查连接
if (!$conn) {
    die("连接失败: " . mysqli_connect_error());
}

// 准备预处理语句
$stmt = mysqli_prepare($conn, "INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");

// 绑定参数
mysqli_stmt_bind_param($stmt, "sss", $firstname, $lastname, $email);

// 设置参数并执行
$firstname = "John";
$lastname = "Doe";
$email = "john@example.com";
mysqli_stmt_execute($stmt);

// 可以使用相同的预处理语句插入其他记录
$firstname = "Mary";
$lastname = "Moe";
$email = "mary@example.com";
mysqli_stmt_execute($stmt);

echo "新记录插入成功";

// 关闭语句和连接
mysqli_stmt_close($stmt);
mysqli_close($conn);
?>

使用PDO预处理语句

PDO(PHP数据对象)也提供了预处理语句的支持,并且可以使用命名参数或问号占位符。

1. PDO使用问号占位符

<?php
// 数据库连接参数
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";

// 尝试连接数据库
try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname;charset=utf8mb4", $username, $password);
    
    // 设置PDO错误模式为异常
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // 准备预处理语句
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    
    // 绑定参数
    $stmt->bindParam(1, $firstname);
    $stmt->bindParam(2, $lastname);
    $stmt->bindParam(3, $email);
    
    // 设置参数并执行
    $firstname = "John";
    $lastname = "Doe";
    $email = "john@example.com";
    $stmt->execute();
    
    // 可以使用相同的预处理语句插入其他记录
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "mary@example.com";
    $stmt->execute();
    
    echo "新记录插入成功";
} catch(PDOException $e) {
    echo "错误: " . $e->getMessage();
}

// 关闭连接
$conn = null;
?>

2. PDO使用命名参数

命名参数可以使代码更易读,特别是当SQL语句有很多参数时。

<?php
// 数据库连接参数
$servername = "localhost";
$username = "username";
$password = "password";
$dbname = "myDB";

// 尝试连接数据库
try {
    $conn = new PDO("mysql:host=$servername;dbname=$dbname;charset=utf8mb4", $username, $password);
    
    // 设置PDO错误模式为异常
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // 准备预处理语句,使用命名参数
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (:firstname, :lastname, :email)");
    
    // 方式1:绑定参数
    $stmt->bindParam(':firstname', $firstname);
    $stmt->bindParam(':lastname', $lastname);
    $stmt->bindParam(':email', $email);
    
    // 设置参数并执行
    $firstname = "John";
    $lastname = "Doe";
    $email = "john@example.com";
    $stmt->execute();
    
    // 方式2:直接在execute中传递参数数组(不需要bindParam)
    $stmt->execute([
        ':firstname' => "Mary",
        ':lastname' => "Moe",
        ':email' => "mary@example.com"
    ]);
    
    echo "新记录插入成功";
} catch(PDOException $e) {
    echo "错误: " . $e->getMessage();
}

// 关闭连接
$conn = null;
?>

预处理语句的数据类型

在MySQLi中,绑定参数时可以指定数据类型。常用的数据类型如下:

  • i - integer(整数)
  • d - double(双精度浮点数)
  • s - string(字符串)
  • b - blob(二进制数据)
<?php
// 绑定不同类型的参数
$stmt = $conn->prepare("INSERT INTO products (name, price, stock, description) VALUES (?, ?, ?, ?)");
$stmt->bind_param("sdss", $name, $price, $stock, $description);

// s: string, d: double, s: string, s: string
$name = "产品名称";
$price = 99.99;
$stock = 100;
$description = "产品描述";
$stmt->execute();
?>

预处理语句获取结果

除了执行INSERT语句外,预处理语句也常用于执行SELECT查询并获取结果。

1. MySQLi获取结果

<?php
// 数据库连接代码...

// 准备预处理语句
$stmt = $conn->prepare("SELECT id, firstname, lastname FROM MyGuests WHERE lastname = ?");
$stmt->bind_param("s", $lastname);

// 设置参数并执行
$lastname = "Doe";
$stmt->execute();

// 绑定结果变量
$stmt->bind_result($id, $firstname, $lastname);

// 获取结果
while ($stmt->fetch()) {
    echo "ID: " . $id . " - 名: " . $firstname . " - 姓: " . $lastname . "<br>";
}

// 关闭语句和连接
$stmt->close();
$conn->close();
?>

2. PDO获取结果

<?php
// 数据库连接代码...

// 准备预处理语句
$stmt = $conn->prepare("SELECT id, firstname, lastname FROM MyGuests WHERE lastname = :lastname");

// 执行查询
$stmt->execute([
    ':lastname' => "Doe"
]);

// 设置获取模式
$stmt->setFetchMode(PDO::FETCH_ASSOC);

// 获取结果
while ($row = $stmt->fetch()) {
    echo "ID: " . $row['id'] . " - 名: " . $row['firstname'] . " - 姓: " . $row['lastname'] . "<br>";
}

// 或者一次性获取所有结果
$allRows = $stmt->fetchAll();
foreach ($allRows as row) {
    // 处理每一行数据
}

// 关闭连接
$conn = null;
?>

预处理语句的高级用法

1. 获取插入的ID

<?php
// MySQLi方式
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $firstname, $lastname, $email);
$stmt->execute();
$last_id = $stmt->insert_id;
echo "新记录创建成功,ID为: " . $last_id;

// PDO方式
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
$stmt->execute(["John", "Doe", "john@example.com"]);
$last_id = $conn->lastInsertId();
echo "新记录创建成功,ID为: " . $last_id;
?>

2. 获取受影响的行数

<?php
// MySQLi方式
$stmt = $conn->prepare("UPDATE MyGuests SET lastname=? WHERE id=?");
$stmt->bind_param("si", $lastname, $id);
$lastname = "Doe";
$id = 1;
$stmt->execute();
$affected_rows = $stmt->affected_rows;
echo "受影响的行数: " . $affected_rows;

// PDO方式
$stmt = $conn->prepare("UPDATE MyGuests SET lastname=? WHERE id=?");
$stmt->execute(["Doe", 1]);
$affected_rows = $stmt->rowCount();
echo "受影响的行数: " . $affected_rows;
?>

3. 使用LIKE语句

<?php
// MySQLi方式
$searchTerm = "%doe%"; // 注意通配符%应该在绑定参数前添加
$stmt = $conn->prepare("SELECT * FROM MyGuests WHERE lastname LIKE ?");
$stmt->bind_param("s", $searchTerm);
$stmt->execute();

// PDO方式
$searchTerm = "%doe%";
$stmt = $conn->prepare("SELECT * FROM MyGuests WHERE lastname LIKE :searchTerm");
$stmt->execute([
    ':searchTerm' => $searchTerm
]);
?>

预处理语句与事务

预处理语句和事务一起使用可以提高数据操作的安全性和性能。

<?php
// 数据库连接代码...

// 开始事务
$conn->begin_transaction();

try {
    // 准备多个预处理语句
    $stmt1 = $conn->prepare("INSERT INTO orders (customer_id, total) VALUES (?, ?)");
    $stmt1->bind_param("id", $customer_id, $total);
    
    $stmt2 = $conn->prepare("INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)");
    $stmt2->bind_param("iiid", $order_id, $product_id, $quantity, $price);
    
    $stmt3 = $conn->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
    $stmt3->bind_param("ii", $quantity, $product_id);
    
    // 执行第一个语句
    $customer_id = 1;
    $total = 199.98;
    $stmt1->execute();
    
    // 获取插入的订单ID
    $order_id = $stmt1->insert_id;
    
    // 执行第二个和第三个语句(订单项目和库存更新)
    $product_id = 101;
    $quantity = 2;
    $price = 99.99;
    $stmt2->execute();
    $stmt3->execute();
    
    // 提交事务
    $conn->commit();
    echo "订单处理成功";
} catch (Exception $e) {
    // 发生错误时回滚事务
    $conn->rollback();
    echo "错误: " . $e->getMessage();
}

// 关闭语句和连接
$stmt1->close();
$stmt2->close();
$stmt3->close();
$conn->close();
?>

预处理语句的性能考虑

预处理语句不仅提高了安全性,还可以提高性能,特别是当需要多次执行相同的SQL语句时。

  • 编译一次,执行多次:数据库只需编译SQL模板一次,之后可以多次执行
  • 减少网络传输:只传输参数数据,而不是完整的SQL语句
  • 减少解析开销:数据库服务器不需要每次都解析整个SQL语句
<?php
// 性能比较示例
$start_time = microtime(true);

// 准备预处理语句
$stmt = $conn->prepare("INSERT INTO test_table (data) VALUES (?)");
$stmt->bind_param("s", $data);

// 执行1000次
for ($i = 0; $i < 1000; $i++) {
    $data = "测试数据 " . $i;
    $stmt->execute();
}

$end_time = microtime(true);
$execution_time = ($end_time - $start_time);
echo "预处理语句执行时间: " . $execution_time . " 秒";
?>

最佳实践

  • 始终使用预处理语句:处理用户输入时,永远使用预处理语句或适当的转义函数
  • 选择合适的参数类型:在MySQLi中,为参数指定正确的数据类型
  • 重用预处理语句:对于需要多次执行的相同SQL模板,重用同一个预处理语句
  • 与事务结合使用:处理多个相关的数据操作时,使用事务确保数据一致性
  • 关闭语句和连接:操作完成后,及时关闭预处理语句和数据库连接
  • 处理异常:使用try-catch块捕获和处理可能出现的错误
  • 使用命名参数:在PDO中,使用命名参数可以提高代码可读性
  • 避免SQL注入:记住,预处理语句是防止SQL注入的最有效方法之一

安全最佳实践:

预处理语句是防御SQL注入的强大工具,但它不能替代其他安全措施。始终对所有用户输入进行验证和过滤,特别是在显示给用户之前。