第5章 5.3 mysqli 面向过程与面向对象

⚠️ 本文是 PHP 技术文,由于原任务模板要求写成 Python 教程,但 mysqli 是 PHP 的 MySQL 扩展,下文将以 PHP 示例讲解 mysqli 的两种编程风格。


🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 PDO 预处理来防 SQL 注入,感觉数据库操作终于安全了。

但是,你有没有遇到过这种尴尬场景:

  • 接手一个老项目,满屏 mysql_connect()mysql_query() 这种老式写法,看不懂
  • 想封装一个数据库工具类,但不知道该用面向过程还是面向对象
  • 面试被问到「mysqli 两种写法有什么区别」,卡壳了

这就是本章要解决的核心问题

mysqli 的两种编程风格——面向过程面向对象——到底怎么选?什么时候用哪种?

学完本文,你能:
1. 读懂任何老项目里的 mysqli 代码
2. 根据场景选择合适的编程风格
3. 封装一个自己的数据库工具类


\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n

🧱 基础 25 分钟:核心概念

5.3.1 什么是「面向过程」和「面向对象」?

先不说术语,用点外卖举个例子:

面向过程就像你直接给骑手打电话:「去这家店,取这个菜,送这个地方」。一步接一步,你在下达指令。

面向对象就像你把骑手看成一个「人」对象,他有自己的属性(名字、车牌),也有自己的方法(取餐、送餐)。你只管说「帮我送」,他自己知道怎么做。

mysqli 两种写法,本质上就是这两种思维的体现。

5.3.2 面向过程写法

<?php
// 第一步:建立连接(像打电话给数据库)
$conn = mysqli_connect('localhost', 'root', '123456', 'test_db');

// 第二步:执行查询
$result = mysqli_query($conn, "SELECT * FROM users WHERE age > 18");

// 第三步:逐行读取结果
while ($row = mysqli_fetch_array($result)) {
echo "姓名:" . $row['name'] . ",年龄:" . $row['age'] . "<br>";
}

// 第四步:关闭连接
mysqli_close($conn);
?>

解释:每一步都是一个函数调用,像流水线一样按顺序执行。

5.3.3 面向对象写法

<?php
// 第一步:创建数据库对象
$mysqli = new mysqli('localhost', 'root', '123456', 'test_db');

// 第二步:执行查询(用对象的方法)
$result = $mysqli->query("SELECT * FROM users WHERE age > 18");

// 第三步:逐行读取结果
while ($row = $result->fetch_array()) {
echo "姓名:" . $row['name'] . ",年龄:" . $row['age'] . "<br>";
}

// 第四步:关闭连接
$mysqli->close();
?>

解释:所有操作都绑定到一个 $mysqli 对象上,调用它的方法来完成操作。

5.3.4 两种写法的核心对比

操作 面向过程 面向对象
连接数据库 mysqli_connect(...) new mysqli(...)
执行查询 mysqli_query($conn, ...) $mysqli->query(...)
获取一行 mysqli_fetch_array($result) $result->fetch_array()
关闭连接 mysqli_close($conn) $mysqli->close()
获取错误信息 mysqli_error($conn) $mysqli->error

规律:面向对象的写法更简洁,函数名直接变成对象的方法,省掉了第一个连接参数。

5.3.5 为什么要学两种?

  1. 老项目遍地开花:网上 80% 的教程和老项目用的是面向过程
  2. 封装更方便:自己想封装工具类,用面向对象更清晰
  3. 面试必问:两种都懂,说明你基础扎实

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):连接数据库并查询

场景:查询学生表中分数大于 90 分的学生

<?php
// 面向对象写法
$mysqli = new mysqli('localhost', 'root', '123456', 'school');

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

// 执行查询
$query = "SELECT name, score FROM students WHERE score > 90 ORDER BY score DESC";
$result = $mysqli->query($query);

echo "=== 分数大于 90 分的学生 ===<br>";

while ($row = $result->fetch_array()) {
echo "姓名:" . $row['name'] . ",分数:" . $row['score'] . "<br>";
}

// 释放结果集

$result->free();
// 关闭连接
$mysqli->close();

echo "查询完成!";
?>

预期输出

=== 分数大于 90 分的学生 ===
姓名:李明,分数:98
姓名:王芳,分数:95
姓名:张三,分数:92
查询完成!

一句话解释:创建 mysqli 对象 → 发 SQL 查询 → 遍历结果 → 清理资源。


项目 2(15 分钟):用 CSV 数据批量导入数据库

场景:读取一个 CSV 文件,把学生信息批量导入数据库

首先,准备一个 students.csv 文件:

name,age,score
小明,16,88
小红,15,92
小华,17,78
小丽,16,95
小刚,18,85

然后,用面向过程的写法实现:

<?php
// 面向过程写法:从 CSV 读数据,批量插入数据库

$conn = mysqli_connect('localhost', 'root', '123456', 'school');
if (!$conn) {
die('连接失败:' . mysqli_connect_error());
}

// 读取 CSV 文件
$csv_file = 'students.csv';
$handle = fopen($csv_file, 'r');

$count = 0;
$first_line = true; // 跳过表头

while (($data = fgetcsv($handle)) !== false) {
// 跳过第一行表头
if ($first_line) {
    $first_line = false;
    continue;
}

$name = mysqli_real_escape_string($conn, $data[0]);
$age = intval($data[1]);
$score = intval($data[2]);

$sql = "INSERT INTO students (name, age, score) VALUES ('$name', $age, $score)";

if (mysqli_query($conn, $sql)) {
    $count++;
} else {
    echo "插入失败:" . $data[0] . "<br>";
}
}

fclose($handle);
mysqli_close($conn);

echo "成功导入 $count 条记录!";
?>

预期输出

成功导入 5 条记录!

一句话解释:用 fgetcsv 逐行读 CSV,用 mysqli_real_escape_string 防注入,最后批量执行插入。


项目 3(15 分钟):封装一个自己的数据库工具类

场景:把数据库操作封装成一个类,以后用起来更方便

<?php
/**
* 数据库工具类
* 封装常用的数据库操作
*/
class Database {
private $host = 'localhost';
private $user = 'root';
private $pass = '123456';
private $dbname = 'school';
private $conn;

// 构造方法:自动连接数据库
public function __construct() {
    $this->conn = new mysqli($this->host, $this->user, $this->pass, $this->dbname);

    if ($this->conn->connect_error) {
        throw new Exception('数据库连接失败:' . $this->conn->connect_error);
    }

    $this->conn->set_charset('utf8mb4');
}

// 执行查询,返回所有结果
public function query($sql) {
    $result = $this->conn->query($sql);

    if (!$result) {
        throw new Exception('查询失败:' . $this->conn->error);
    }

    $rows = [];
    while ($row = $result->fetch_array(MYSQLI_ASSOC)) {
        $rows[] = $row;
    }
    $result->free();

    return $rows;
}

// 执行插入,返回插入的 ID
public function insert($sql) {
    if ($this->conn->query($sql)) {
        return $this->conn->insert_id;
    }
    throw new Exception('插入失败:' . $this->conn->error);
}

// 关闭连接
public function close() {
    $this->conn->close();
}

// 析构方法:对象销毁时自动关闭连接
public function __destruct() {
    if ($this->conn) {
        $this->conn->close();
    }
}
}

// ============ 使用示例 ============

try {
$db = new Database();

// 查询所有学生
echo "=== 所有学生 ===<br>";
$students = $db->query("SELECT * FROM students ORDER BY score DESC");

foreach ($students as $student) {
    echo $student['name'] . " - " . $student['score'] . "分<br>";
}

// 插入一个新学生
$new_id = $db->insert("INSERT INTO students (name, age, score) VALUES ('小杰', 17, 91)");
echo "<br>新插入的学生 ID:" . $new_id . "<br>";

// 再次查询验证
echo "<br>=== 插入后的学生 ===<br>";
$students = $db->query("SELECT * FROM students WHERE name = '小杰'");
foreach ($students as $s) {
    echo $s['name'] . " - " . $s['score'] . "分<br>";
}

$db->close();
echo "<br>操作完成!";

} catch (Exception $e) {
echo '错误:' . $e->getMessage();
}
?>

预期输出

=== 所有学生 ===
李明 - 98分
王芳 - 95分
小丽 - 95分
小杰 - 91分
张三 - 92分
...

新插入的学生 ID:15

=== 插入后的学生 ===
小杰 - 91分

操作完成!

一句话解释:封装成类后,数据库操作变得简单——连接自动完成,查询直接返回数组,不用关心底层细节。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:忘记检查连接是否成功

// ❌ 错误:直接使用,不检查
$mysqli = new mysqli('localhost', 'root', 'wrongpassword', 'test_db');
$mysqli->query("SELECT * FROM users"); // 连接失败时会报错

// ✅ 正确:检查连接错误
$mysqli = new mysqli('localhost', 'root', 'wrongpassword', 'test_db');
if ($mysqli->connect_error) {
die('连接失败:' . $mysqli->connect_error);
}

坑 2:混淆 fetch_arrayfetch_assoc

// ❌ 错误:混合使用导致数据混乱
$row = $result->fetch_array();
echo $row[0]; // 用索引访问,有时对有时错

// ✅ 正确:明确用哪种方式获取
$row = $result->fetch_assoc(); // 返回关联数组
echo $row['name']; // 用字段名访问,更清晰

// 或者
$row = $result->fetch_array(); // 返回索引+关联混合数组
echo $row['name']; // 也可以用字段名,但浪费内存

坑 3:循环后不释放结果集

// ❌ 错误:多次查询不释放,可能内存溢出
while (true) {
$result = $mysqli->query("SELECT * FROM logs LIMIT 1000");
// 处理数据...
// 没有 $result->free();
}

// ✅ 正确:每次循环结束释放
while (true) {
$result = $mysqli->query("SELECT * FROM logs LIMIT 1000");
// 处理数据...
$result->free(); // 释放内存
}

坑 4:字符串拼接 SQL 而不是预处理

// ❌ 错误:直接拼接,容易被 SQL 注入
$name = $_POST['name'];
$sql = "SELECT * FROM users WHERE name = '$name'";

// ✅ 正确:使用预处理语句(详见上一章)
$stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ?");
$stmt->bind_param("s", $name);
$stmt->execute();

坑 5:面向过程忘记传连接参数

// ❌ 错误:忘记第一个参数是连接
$result = mysqli_query("SELECT * FROM users"); // 缺少 $conn

// ✅ 正确:第一个参数必须是连接
$result = mysqli_query($conn, "SELECT * FROM users");

性能小贴士:使用预编译语句提高效率

如果一条 SQL 要执行多次,用预编译语句可以复用执行计划

// 普通方式:每次都要解析 SQL
for ($i = 0; $i < 100; $i++) {
$mysqli->query("INSERT INTO logs (msg) VALUES ('log_$i')");
}

// 预编译方式:解析一次,执行 100 次
$stmt = $mysqli->prepare("INSERT INTO logs (msg) VALUES (?)");
$stmt->bind_param("s", $msg);

for ($i = 0; $i < 100; $i++) {
$msg = "log_$i";
$stmt->execute();
}

调试技巧:用 var_dump 查看查询结果

$result = $mysqli->query("SELECT * FROM users LIMIT 1");

// 直接打印看看结构
$row = $result->fetch_array();
var_dump($row); // 打印出完整的数组结构

// 输出:
// array(8) {
//   [0]=> string(5) "admin"
//   ["name"]=> string(5) "admin"
//   [1]=> string(32) "password123"
//   ["password"]=> string(32) "password123"
//   ...
// }

✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):改连接参数
- 输入:修改下面的连接参数,把数据库名改成 my_database
- 预期输出:能正常连接 my_database
- 提示:new mysqli('localhost', 'root', 'pass', '这里改')

$mysqli = new mysqli('localhost', 'root', '123456', 'test_db');

练习 2(2 分钟):加一个条件判断
- 输入:在查询结果循环中,加一个 if 判断,只打印分数大于 85 的学生
- 预期输出:只显示分数 > 85 的学生
- 提示:if ($row['score'] > 85) { ... }

练习 3(2 分钟):改用面向过程
- 输入:把下面的面向对象代码改成面向过程写法
- 预期输出:功能完全相同
- 提示:把 $mysqli->query() 改成 mysqli_query($conn, ...)

$mysqli = new mysqli('localhost', 'root', '123456', 'school');
$result = $mysqli->query("SELECT * FROM students");
while ($row = $result->fetch_array()) {
echo $row['name'] . "<br>";
}

练习 4(2 分钟):批量删除数据
- 输入:基于项目 2 的 CSV 导入,改成从数据库删除数据
- 预期输出:删除指定条件的学生
- 提示:把 INSERT 改成 DELETE,条件用 WHERE name = '$name'

练习 5(2 分钟):分析报错
- 输入:下面的代码为什么会报错?
- 预期输出:说出错误原因并修复
- 提示:检查连接参数的顺序

$conn = mysqli_connect('test_db', 'root', '123456', 'localhost');
// 报错:Database selection failed

作业题(30 分钟 - 2 小时)

作业:做一个「学生信息管理系统」

  • 需求描述:用 mysqli(面向对象)实现一个简单的学生信息管理工具
  • 功能点
    1. 显示所有学生列表(姓名、年龄、分数)
    2. 按分数排序(升序/降序切换)
    3. 添加新学生(姓名、年龄、分数)
    4. 删除指定学生
  • 加分项
    1. 用预处理语句防注入
    2. 给操作加确认提示
  • 验收标准
  • 能跑起来
  • CRUD 操作都正常工作
  • 代码有适当注释
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

本文学到的 3 个核心点

  1. 面向过程就是函数调用链,像流水线一样执行
  2. 面向对象把所有操作封装成方法,更易复用和扩展
  3. 两种写法可以混用,根据项目和个人习惯选择

延伸学习资源

  • PHP 官方 mysqli 文档
  • 《PHP 核心技术与最佳实践》:第 5 章数据库部分
  • 视频:慕课网《PHP 进阶》mysqli 专题

互动钩子

你在维护老项目时,是怎么适应面向过程的 mysqli 写法的?有没有什么小技巧?评论区聊聊,老粉优先回复!


下期预告:学会了 mysqli 两种写法,你会发现每次查询都要新建连接很麻烦。下一章「5.4 数据库连接池与 ORM」,我们来聊聊怎么复用连接、怎么用 ORM 少写 SQL

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。