当前位置: 首页 > news >正文

2025 PHP7/8 实战入门:15 天精通现代 Web 开发——第 10 课:数据库基础(PDO 实战)

第 10 课:数据库基础(PDO 实战)

一、学习目标

  1. 掌握 PHP 数据操作核心扩展 PDO 的使用方法(替代废弃的mysql_*函数)
  2. 熟练实现数据库 CRUD(增删改查)操作,理解预处理语句防 SQL 注入原理
  3. 运用面向对象思想封装数据库操作类,提升代码复用性
  4. 解决数据库连接、错误处理等实际开发问题

二、核心知识点

(一)PHP 数据库扩展演进与 PDO 优势
  1. 扩展对比与选型

    • 废弃扩展mysql_*(PHP5.5 废弃,PHP7 完全移除),无预处理,安全性低
    • 过渡扩展mysqli_*(支持面向对象和预处理,仅支持 MySQL)
    • 推荐扩展PDO(PHP Data Objects,PHP5.1+,支持多数据库(MySQL、PostgreSQL 等),强制参数绑定,安全性高)

    选型建议:新项目优先使用 PDO,兼顾多数据库兼容性和安全性。

  2. PDO 核心优势

    • 跨数据库兼容性:一套 API 操作多种数据库,切换数据库无需大幅修改代码
    • 预处理语句:通过参数绑定防止 SQL 注入,是 Web 开发安全的核心手段
    • 面向对象设计:支持链式调用,代码更简洁
    • 错误处理:支持异常模式(ERRMODE_EXCEPTION),便于统一捕获错误

    示例(PDO 与 mysqli 连接对比):

    <?php
    // 1. PDO连接MySQL(推荐)
    try {$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', // DSN(数据源名称)'root', // 用户名'', // 密码[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 开启异常模式PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC // 默认关联数组返回]);echo "PDO连接成功<br>";
    } catch (PDOException $e) {die("PDO连接失败:" . $e->getMessage());
    }// 2. mysqli连接MySQL(仅作对比)
    $mysqli = new mysqli('localhost', 'root', '', 'shop');
    if ($mysqli->connect_error) {die("mysqli连接失败:" . $mysqli->connect_error);
    }
    $mysqli->set_charset('utf8mb4');
    echo "mysqli连接成功<br>";
    ?>
    
(二)PDO 核心操作(CRUD)
  1. 查询操作(Read)

    • 核心方法:query()(简单查询)、prepare()+execute()(预处理查询)
    • 结果获取方式:
      • FETCH_ASSOC:关联数组(推荐,字段名作为键)
      • FETCH_OBJ:对象(类似原始文档的mysql_fetch_object
      • FETCH_COLUMN:单列数据
      • FETCH_ALL:获取所有结果(适合小数据集)

    示例(预处理查询用户列表):

    <?php
    try {// 承接上文的$pdo连接// 1. 简单查询(无参数时使用)$stmt = $pdo->query("SELECT id, username, email FROM users ORDER BY id DESC");echo "所有用户:<br>";while ($user = $stmt->fetch()) { // 默认FETCH_ASSOCecho "ID:{$user['id']},用户名:{$user['username']},邮箱:{$user['email']}<br>";}// 2. 预处理查询(带参数,防SQL注入)$user_id = 1;$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); // :id为命名占位符$stmt->execute(['id' => $user_id]); // 绑定参数并执行$user = $stmt->fetch(); // 获取单条结果if ($user) {echo "<br>ID={$user_id}的用户信息:<br>";print_r($user);} else {echo "<br>未找到ID={$user_id}的用户<br>";}// 3. 多参数预处理(查询年龄大于指定值且角色为member的用户)$min_age = 18;$role = 'member';$stmt = $pdo->prepare("SELECT username, age FROM users WHERE age > ? AND role = ?"); // ?为问号占位符$stmt->execute([$min_age, $role]); // 按顺序绑定参数$members = $stmt->fetchAll(); // 获取所有结果echo "<br>年龄大于{$min_age}的{$role}用户:<br>";print_r($members);
    } catch (PDOException $e) {echo "查询失败:" . $e->getMessage();
    }
    ?>
    
  2. 新增操作(Create)

    • 核心:通过预处理语句插入数据,支持获取自增 ID
    • 注意:插入字符串、日期等类型无需手动加引号,PDO 自动处理

    示例(新增用户并获取自增 ID):

    <?php
    try {$user_data = ['username' => 'php8user','password' => password_hash('123456', PASSWORD_DEFAULT), // 密码加密(PHP5.5+)'email' => 'php8@example.com','age' => 22,'role' => 'member','create_time' => date('Y-m-d H:i:s')];// 预处理插入语句$stmt = $pdo->prepare("INSERT INTO users (username, password, email, age, role, create_time)VALUES (:username, :password, :email, :age, :role, :create_time)");// 执行插入(参数数组与占位符对应)$stmt->execute($user_data);// 获取自增ID(新增用户的ID)$new_user_id = $pdo->lastInsertId();echo "新增用户成功,ID:{$new_user_id}";
    } catch (PDOException $e) {echo "新增失败:" . $e->getMessage();
    }
    ?>
    
  3. 更新操作(Update)

    • 核心:必须带WHERE条件,避免全表更新;通过预处理绑定更新值和条件值

    示例(更新用户信息):

    <?php
    try {$update_data = ['email' => 'updated@example.com','age' => 23,'id' => 1 // WHERE条件中的ID];$stmt = $pdo->prepare("UPDATE users SET email = :email, age = :age, update_time = NOW()WHERE id = :id");$stmt->execute($update_data);// 获取受影响的行数$affected_rows = $stmt->rowCount();if ($affected_rows > 0) {echo "更新成功,受影响行数:{$affected_rows}";} else {echo "无数据更新(可能ID不存在或值未变化)";}
    } catch (PDOException $e) {echo "更新失败:" . $e->getMessage();
    }
    ?>
    
  4. 删除操作(Delete)

    • 核心:严格带WHERE条件,避免误删全表;建议先查询再删除(可选)

    示例(删除指定 ID 用户):

    <?php
    try {$user_id = 10;// 先查询用户是否存在(可选,提升用户体验)$stmt = $pdo->prepare("SELECT id FROM users WHERE id = :id");$stmt->execute(['id' => $user_id]);if (!$stmt->fetch()) {echo "删除失败:ID={$user_id}的用户不存在";exit;}// 执行删除$stmt = $pdo->prepare("DELETE FROM users WHERE id = :id");$stmt->execute(['id' => $user_id]);$affected_rows = $stmt->rowCount();echo "删除成功,受影响行数:{$affected_rows}";
    } catch (PDOException $e) {echo "删除失败:" . $e->getMessage();
    }
    ?>
    
(三)PDO 预处理与防 SQL 注入
  1. SQL 注入原理

    • 攻击方式:通过用户输入注入恶意 SQL 片段(如' OR 1=1 #),篡改 SQL 逻辑
    • 示例(未使用预处理的风险):
      // 危险写法:直接拼接用户输入到SQL
      $username = $_GET['username']; // 假设用户输入:' OR 1=1 #
      $sql = "SELECT * FROM users WHERE username = '{$username}'";
      // 最终SQL:SELECT * FROM users WHERE username = '' OR 1=1 #'
      // 后果:查询出所有用户数据,导致信息泄露
      
  2. PDO 预处理防注入机制

    • 核心:SQL 语句与参数分离,参数由 PDO 自动转义,恶意字符失去作用
    • 两种占位符:
      • 命名占位符(:name):适合参数较多的场景,可读性高
      • 问号占位符(?):适合参数较少的场景,简洁

    示例(预处理防注入对比):

    <?php
    // 恶意输入
    $malicious_input = "' OR 1=1 #";// 1. 危险写法(拼接SQL)
    $sql_dangerous = "SELECT * FROM users WHERE username = '{$malicious_input}'";
    echo "危险SQL:{$sql_dangerous}<br>"; 
    // 输出:SELECT * FROM users WHERE username = '' OR 1=1 #'(会查询所有用户)// 2. 安全写法(PDO预处理)
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
    $stmt->execute(['username' => $malicious_input]);
    $result = $stmt->fetchAll();
    echo "预处理查询结果数:" . count($result) . "<br>"; 
    // 输出:0(PDO将恶意输入转义为普通字符串,无匹配用户)
    ?>
    
  3. PDO 安全配置要点

    • 必须开启异常模式:PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,便于捕获错误
    • 禁用模拟预处理:PDO::ATTR_EMULATE_PREPARES => false(强制数据库原生预处理,部分旧版本 MySQL 需开启)
    • 显式设置字符集:DSN 中添加charset=utf8mb4(支持 emoji 表情,避免中文乱码)
(四)面向对象封装数据库操作类
  1. 基础封装思路

    • 封装连接参数(主机、数据库名、用户名、密码)
    • 提供 CRUD 通用方法(queryinsertupdatedelete
    • 统一错误处理(通过异常捕获)
    • 支持事务操作(复杂业务场景必备)
  2. 实战封装示例(PHP8 特性优化)

    <?php
    class DB {// 单例模式:确保只创建一个PDO实例(节省资源)private static ?PDO $pdo = null;// 数据库配置(可从配置文件读取)private const CONFIG = ['host' => 'localhost','dbname' => 'shop','username' => 'root','password' => '','charset' => 'utf8mb4'];// 初始化PDO连接private static function init(): void {if (self::$pdo !== null) {return; // 已连接,直接返回}$dsn = "mysql:host=" . self::CONFIG['host'] . ";dbname=" . self::CONFIG['dbname'] . ";charset=" . self::CONFIG['charset'];$options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES => false // 禁用模拟预处理];try {self::$pdo = new PDO($dsn, self::CONFIG['username'], self::CONFIG['password'], $options);} catch (PDOException $e) {die("数据库连接失败:" . $e->getMessage());}}// 通用查询方法(返回单条结果)public static function getOne(string $sql, array $params = []): array|null {self::init();$stmt = self::$pdo->prepare($sql);$stmt->execute($params);return $stmt->fetch() ?: null;}// 通用查询方法(返回多条结果)public static function getAll(string $sql, array $params = []): array {self::init();$stmt = self::$pdo->prepare($sql);$stmt->execute($params);return $stmt->fetchAll();}// 新增方法(返回自增ID)public static function insert(string $table, array $data): int|false {self::init();$columns = implode(', ', array_keys($data));$placeholders = ':' . implode(', :', array_keys($data));$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";try {$stmt = self::$pdo->prepare($sql);$stmt->execute($data);return (int)self::$pdo->lastInsertId();} catch (PDOException $e) {echo "插入失败:" . $e->getMessage();return false;}}// 更新方法(返回受影响行数)public static function update(string $table, array $data, array $where): int {self::init();$set_clause = [];foreach (array_keys($data) as $key) {$set_clause[] = "{$key} = :{$key}";}$set_clause = implode(', ', $set_clause);$where_clause = [];$where_params = [];foreach ($where as $key => $value) {$where_key = "where_{$key}";$where_clause[] = "{$key} = :{$where_key}";$where_params[$where_key] = $value;}$where_clause = implode(' AND ', $where_clause);$sql = "UPDATE {$table} SET {$set_clause} WHERE {$where_clause}";$params = array_merge($data, $where_params);try {$stmt = self::$pdo->prepare($sql);$stmt->execute($params);return $stmt->rowCount();} catch (PDOException $e) {echo "更新失败:" . $e->getMessage();return 0;}}// 删除方法(返回受影响行数)public static function delete(string $table, array $where): int {self::init();$where_clause = [];$params = [];foreach ($where as $key => $value) {$where_clause[] = "{$key} = :{$key}";$params[$key] = $value;}$where_clause = implode(' AND ', $where_clause);$sql = "DELETE FROM {$table} WHERE {$where_clause}";try {$stmt = self::$pdo->prepare($sql);$stmt->execute($params);return $stmt->rowCount();} catch (PDOException $e) {echo "删除失败:" . $e->getMessage();return 0;}}// 事务开始public static function beginTransaction(): void {self::init();self::$pdo->beginTransaction();}// 事务提交public static function commit(): void {self::init();self::$pdo->commit();}// 事务回滚public static function rollBack(): void {self::init();self::$pdo->rollBack();}
    }// 类使用示例
    // 1. 查询
    $user = DB::getOne("SELECT username, email FROM users WHERE id = :id", ['id' => 1]);
    echo "查询用户:<br>";
    print_r($user);// 2. 新增
    $new_id = DB::insert('users', ['username' => 'pdouser','password' => password_hash('654321', PASSWORD_DEFAULT),'email' => 'pdo@example.com','age' => 25,'role' => 'member','create_time' => date('Y-m-d H:i:s')
    ]);
    echo "<br>新增用户ID:{$new_id}";// 3. 更新
    $update_rows = DB::update('users', ['age' => 26], // 要更新的数据['id' => $new_id] // WHERE条件
    );
    echo "<br>更新行数:{$update_rows}";// 4. 删除
    $delete_rows = DB::delete('users', ['id' => $new_id]);
    echo "<br>删除行数:{$delete_rows}";
    ?>
    

三、注意事项

  1. 数据库连接问题

    • 避免在脚本中硬编码数据库密码,建议存放在独立配置文件(如config/db.php),并限制文件访问权限
    • 生产环境中禁用 root 用户连接数据库,创建专用数据库用户并分配最小权限(如仅SELECT/INSERT权限)
    • 长时间运行的脚本(如定时任务)需处理连接超时问题,可通过PDO::ATTR_TIMEOUT设置超时时间
  2. 字符集与编码

    • 必须在 DSN 中显式指定charset=utf8mb4(而非utf8),utf8mb4支持完整的 UTF-8 编码(包括 emoji 表情)
    • 数据库表和字段的编码也需设置为utf8mb4,避免中文乱码
  3. 事务使用场景

    • 当一个业务需要执行多个 SQL 操作(如转账:扣减 A 账户→增加 B 账户),必须使用事务确保原子性(要么全成功,要么全失败)
    • 事务中若执行SELECT FOR UPDATE(行锁),需注意避免死锁,控制事务执行时间
  4. 性能优化

    • 高频查询字段需建立索引(如users表的idusername字段)
    • 避免SELECT *,只查询需要的字段,减少数据传输量
    • 大量数据查询使用分页(LIMIT offset, count),避免一次性加载过多数据

四、实战练习

  1. 创建day10文件夹,新建user_crud.php文件:

    • 基于上文封装的DB类,实现一个完整的用户管理功能,包含:
      • 用户列表页:分页显示所有用户(每页 10 条,使用LIMIT实现),显示用户名、邮箱、年龄、角色、创建时间
      • 用户添加页:表单提交用户名、密码、邮箱、年龄、角色,密码需用password_hash加密,表单需验证(非空、邮箱格式等)
      • 用户编辑页:根据 ID 查询用户信息并回显到表单,支持修改邮箱、年龄、角色(不允许修改用户名和密码)
      • 用户删除功能:点击删除按钮时弹出确认框,确认后执行删除(需防误删,可先查询用户信息确认)
      • 所有操作需通过 PDO 预处理实现,确保防 SQL 注入
  2. 新建order_transaction.php文件:

    • 模拟电商订单创建场景,使用事务确保数据一致性:
      • 业务逻辑:创建订单(orders表插入)→ 扣减商品库存(products表更新)→ 记录订单商品(order_products表插入)
      • 要求:三个操作必须全部成功,若任意一步失败则回滚所有操作
      • 模拟失败场景:故意设置库存不足,观察事务是否回滚(库存不变,订单不创建)
      • 记录操作日志:成功 / 失败信息写入logs/order.log,包含时间、订单号、操作结果
http://www.dtcms.com/a/407707.html

相关文章:

  • 跟进 JDK25:将虚拟线程安全引入生产的权衡与实战
  • 网站浏览历史怎么查看欧美网站建设公司排名
  • 四平网站建设哪家效果好wordpress 中文博客主题
  • 小灯题目!!
  • RK3588:MIPI底层驱动学习——入门第三篇(IIC与V4L2如何共存?)
  • 去哪儿面经
  • 网站建设和维护合同iis7 网站打不开
  • EPLAN2022部件库导入步骤
  • 微信网站建设报价表网页设计文案
  • < 自用文 备份 script :intar.sh> 使用 tar cvfz 命令打包成 .tar.gz 文件来备份多目标(目录,文件)
  • 网站建设asp做家具的网站
  • 高校网站建设的时效性浅谈网站建设
  • 卫星互联网商用落地的三个条件
  • 网站分析流程全美网站建设公司
  • 中讯高科网站建设襄阳网站推广优化技巧
  • 中值滤波器原理及C++实现
  • 【 GUI自动化测试】pywinauto 常见操作
  • SQL SELECT 语句怎么用?COMPANY 表查询案例(含条件 / 模糊 / 分页)
  • 北京网站建设公司降龙手机创建个人网站 免费
  • 朝阳企业网站建设方案高端办公室装修公司
  • 写网站开发代码注册公司地址可以是住宅吗
  • 网站 底部网站建设做得好
  • 老字号传承,达尔优AE6电竞鼠标!熟悉的味道,时代的配方
  • 通过你的自有服务器代理网址
  • 智能手机背面缺陷检测数据集VOC+YOLO格式5203张5类别
  • 太乙笔记全文
  • 成功网站管理系统个人网页设计硬件需求
  • 淄博网站排名优化公司网站建设进度总结
  • 想建设一个网站济南建站
  • 网站地区词优化智能网站推广优化