PDO 是什么
PDO 是 PHP Date Object(PHP 数据对象)的简称,它是 PHP 为访问数据库定义的一个轻量级的、一致性的接口,它提供了一个数据访问抽象层,这样无论你使用什么数据库,都可以通过同一函数执行查询和获取数据,大大简化了数据库的操作,并能够屏蔽不同数据库之间的差异。
PDO 是与 PHP5.1 版本一起发行的,使用 PDO 可以很方便地进行跨数据库程序的开发,以及不同数据库间的移植,目前 PDO 支持的数据库包括 Firebird、FreeTDS、Interbase、MySQL、SQL Server、ODBC、Oracle、Postgre SQL、SQLite 和 Sybase 等。
PDO 的特点
我们可以将 PDO 看作是一个“数据库访问抽象层”,作用是统一各种数据库的访问接口。与 MySQL 和 MSSQL 函数库相比,PDO 让跨数据库的使用更具有亲和力,与 ADODB 和 MDB2 相比,PDO 更加高效。
PDO 将通过一种轻型、清晰、方便的函数,统一各种不同的数据库的共有特性,实现 PHP 脚本在最大程度上的抽象性和兼容性。
PDO 吸取了现有数据库扩展成功和失败的经验教训,利用 PHP5 的最新特性,可以轻松地与各种数据库进行交互。
PDO 扩展是模块化的,能够在运行时为用户数据库后端加载驱动程序,而不必重新编译或重新安装整个 PHP 程序。例如,PDO_MySQL 扩展会替代 PDO 扩展实现 MySQL 数据库 API,它还有一些用于 Oracle、Postgre SQL、ODBC 和 Firebird 的驱动程序。
开启 PDO扩展
默认情况下,PDO 在 PHP 中为开启状态,但是要启用对某个数据库驱动程序的支持,仍需要进行相应的配置操作。
以 Windows 系统下为例,在配置文件 php.ini 中有关 PDO 相关的配置信息如下所示:
;extension=pdo_firebird;extension=php_pdo_mysql.dll;extension=pdo_oci;extension=pdo_odbc;extension=pdo_pgsql;extension=pdo_sqlite
提示:开启相应的配置只需要去除配置项前面的分号;,然后重启 Apache 服务器即可。
验证相关的配置是否开启成功,只需要执行 phpinfo() 函数就行,在输出的页面中搜索配置的名称,如果存在则说明开启成功
使用PDO连接数据库
在使用 PDO 与不同数据库之间交互时,PDO 对象中的成员方法是统一各种数据库的访问接口,所以在使用 PDO 与数据库交互之前,首先要创建一个 PDO 对象,然后再通过对象的构造函数来连接数据库。
new PDO(string $dsn[, string $username [, string $password [, array $driver_options]]]);
参数说明如下:
$dsn:数据源名称或叫做 DSN(Data Source Name 的缩写),包含了请求连接到数据库的信息。通常一个 DSN 是由 PDO 驱动程序的名称,紧随其后是一个冒号,再后面是可选的驱动程序的数据库连接信息,比如主机名、端口和数据库名。以 MySQL 数据库为例 $dsn 可以定义为:
mysql:host=localhost;port=3306;dbname=dbname;charset=utf8
,分别定义了数据库类型、端口号、数据库名和字符集;$username:可选参数,用来表示 DSN 字符串中的用户名;
$password:可选参数,用来表示 DSN 字符串中的密码;
$driver_options:可选参数,一个具体驱动的连接选项的键/值数组。
PHP 数据对象:https://www.php.net/manual/zh/book.pdo.php
预定义常量:https://www.php.net/manual/zh/pdo.constants.php
创建 PDO 对象
$pdo = new PDO($dsn, $user, $pwd);
参数:dsn,数据库用户名,数据库密码
<?php $type = 'mysql'; //数据库类型 $host = 'localhost'; //数据库主机名 $dbname = 'test'; //使用的数据库名称 $username = 'root'; //数据库连接用户名 $username = 'root'; //数据库连接密码 $dsn="$type:host=$host;dbname=$dbname"; try{ // 可选,设置错误提示级别为WARNING $params = array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_WARNING); //初始化一个PDO对象 $pdo= new PDO($dsn,$user,$pwd,$params); # 设置结果集的默认获取的方式 (默认索引数组和关联数组) $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC); }catch(Exception $e){ die('数据库连接失败:'.$e -> getMessage()); }?>
使用PDO执行SQL语句
在 PDO 中,我们可以使用三种方式来执行 SQL 语句,分别是 exec() 方法,query() 方法,以及预处理语句 prepare() 和 execute() 方法。
执行sql语句: exec() 、query()、 perpare();
1、query用来处理有结果集的,如select, 返回 PDOStatement 对象,失败返回false(当为 PDO::ERRMODE_SILENT,这也是默认的值)
2、exec用来处理有返回影响行数的(int),如 insert(插入的行数)、 delete(删除的行数) 、update(和原数值不等才算), 失败返回false (当为 PDO::ERRMODE_SILENT,这也是默认的值)
3、prepare 执行所有sql,可以完全替代 query,exec的功能,并且可以防止SQL注入
错误报告是针对执行的sql出错时
PDO::ERRMODE_SILENT(0) :默认 不提示任何错误 ,连接时无论如何都会提示,只有在执行后面的方法时才会起作用
PDO::ERRMODE_WARNING(1) : 警告
PDO::ERRMODE_EXCEPTION(2):异常(推荐使用) 用try catch捕获,也可以手动抛出异常 new PDOException($message, $code, $previous)
1) exec() 方法
当执行 INSERT、UPDATE 和 DELETE 等不需要返回结果集的 SQL 语句时,可以使用 PDO 对象中的 exec() 方法。该方法成功执行后,将返回受影响的行数
主要思路:
(1)连接数据库、数据库的用户名、数据库的密码
(2)生成PDO对象
(3)执行查询
<?php # $stmt = $pdo->exec($sql); 执行SQL增删改语句,返回值是false或 受影响的整型数量 $dsn = 'mysql:host=localhost;dbname=users;'; $username = 'root'; $password = 'root'; // 生成PDO对象 $pdo = new PDO($dsn,$user,$pwd); $sql = "insert into user(name,age,sex) values('zhang','18','男')"; $res = $pdo -> exec($sql); if($res) echo '成功添加 '.$res.' 条数据!';?>
# exec用法try { $sql = "insert into users (`user_name`, `user_pwd`) values('zhang', '123'),('admin', 'admin')"; $rows = $pdo->exec($sql); // 影响的条数 2 $id = $pdo->lastInsertId(); //最后插入的id,有多条时返回的是第一条的id} catch (Exception $e) { echo $pdo->errorInfo();}
注释:exec主要用于执行没有返回结果集的操作,比如insert、delete、update,返回的是影响的记录条数
2) query() 方法
当执行需要返回结果集的 SELECT 查询语句时,可以使用 PDO 对象中的 query() 方法。如果该方法执行成功,则会返回一个 PDOStatement 对象。如果使用了query() 方法,并想了解获取的数据行总数,可以使用 PDOStatement 对象中的 rowCount() 方法获取。
PDO::query(string $sql)# 执行SQL查询语句,返回值是false或 PDOStatement 类型的对象$stmt = $pdo->query($sql); # 读取结果中的数量echo $stmt->rowCount();# 返回结果集第一条数据$stmt->fetch(PDO::FETCH_ASSOC);# 返回结果集全部数据$stmt->fetchAll(PDO::FETCH_ASSOC);# 直给pdo对象全局设置返回类型$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC);
通过设置第二个参数可以调整返回值的样式:
PDO::FETCH_BOTH 关联+索引数组(默认值)
PDO::FETCH_ASSOC 关联数组
PDO::FETCH_NUM 索引数组
PDO::FETCH_OBJ 对象类型
<?php $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); $sql = "SELECT * FROM users "; // 获取PDOStatement对象 $stmt = $pdo -> query($sql,PDO::FETCH_ASSOC); echo "<pre>"; print_r($stmt); // 获取结果集全部数据 print_r($stmt->fetchAll());?>
设置结果集参数也可以通过fetch和fetchAll()传入
<?php $sql = "SELECT * FROM users "; // 获取PDOStatement对象 $stmt = $pdo -> query($sql); // 获取结果集中第一条数据 返回关联数组 $res = $stmt->fetchAll(PDO::FETCH_ASSOC); echo "<pre>"; print_r($res);?>
为了避免每次获取结果集都反复的设置参数, setAttribute()方法是设置部分属性我们可以直接在连接数据库时给pdo对象统一的来设置
<?php $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); # 设置结果集的默认获取的方式 (默认索引数组和关联数组) $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC); $sql = "SELECT * FROM users "; // 获取PDOStatement对象 $stmt = $pdo -> query($sql); // 获取结果集中第一条数据 返回关联数组 $res = $stmt->fetchAll(); echo "<pre>"; print_r($res);?>
使用 query() 和 exec() 方法有以下几点需要注意:
query() 和 exec() 都可以执行所有的 SQL 语句,只是返回值不同而已;
query() 可以实现所有 exec() 的功能;
当把 select 语句应用到 exec() 时,总是返回 0;
如果要看查询的具体结果,可以通过 foreach 语句完成循环输出。
3) 预处理语句方式
当同一个查询需要多次执行时(有时需要迭代传入不同的条件参数),使用预处理语句的方式来实现效率会更高。使用预处理语句就需要使用 PDO 对象中的 prepare() 方法去准备一个将要执行的查询,再使用 PDOStatement 对象中的 execute() 方法来执行。
$stmt = $pdo -> prepare($sql);$stmt -> execute([参数1,参数2]);$args = $stmt->fetchAll(参数):读取查询结果数组$arg = $stmt->fetch(参数):读取一条查询结果数组,参数同上$obj = $stmt->fetchObject():直接读取一条结果为对象类型$pdo->errorInfo():读取错误信息,返回值是数组类型
SQL 语句模板中可以包含零个或多个参数占位标记,格式可以是命名(:name)或问号(?)的形式,当它执行时将用真实数据取代。在同一个 SQL 语句里,命名和问号形式不能同时使用,只能选择其中一种参数形式。如果使用命名形式的占位标记,那么标记的命名必须是唯一的。使用预处理语句可以有效的避免传统的方式中SQL注入问题
使用命名形式的参数占位符,查询指定的 SQL 语句
<?php #无序方式 命名占位符 $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); $sql = "SELECT user_name,user_email FROM users WHERE user_sex = :sex"; $sth = $pdo -> prepare($sql); $sth -> execute([':sex'=>0]); $res = $sth -> fetchAll(); echo '<pre>'; print_r($res);?>
使用问号形式的参数占位符,查询指定的 SQL 语句
<?php #有序方式 问号占位符 $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); $sql = "SELECT user_name,user_email FROM users WHERE user_sex= ?"; $sth = $pdo -> prepare($sql); $sth -> execute([0]); $res = $sth -> fetchAll(PDO::FETCH_ASSOC); echo '<pre>'; print_r($res);?>
使用PDOStatement::bindParam() 提前引用绑定参数,而不是执行的时候绑定参数。
// 连接数据库$dsn = "mysql:host=127.0.0.1;port=3306;dbname=mydb";$opts = array(PDO::ATTR_AUTOCOMMIT=>0, PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_AUTOCOMMIT=>0);try { $pdo = new PDO($dsn, 'root', 'root', $opts);}catch(PDOException $e){ echo $e->getMessage();}/* pdo中有两种占位符号 ** ? 参数占位符 --- 索引数组, 按索引顺序使用* 命名参数占位符 ----关联数组, 按名称使用,和顺序无关*///准备好了一条语句,并入到服务器端,也已经编译过来了,就差为它分配数据过来//同样适用于更新操作// 命名参数占位符$stmt=$pdo->prepare("insert into users (`user_name`, `user_pwd`,'user_email','create_time') values(:name,:pwd,:email,:time)");//绑定参数,引用方式传递$stmt->bindParam(":name", $name);$stmt->bindParam(":pwd", $pwd);$stmt->bindParam(":email", $email);$stmt->bindParam(":time", $time);#变量放到 bindParam 前后都可$name="zhang";$pwd = '1234';$email = 'zhang@qq.com';$time = time();// 问号参数占位符$stmt=$pdo->prepare("insert into users (`user_name`, `user_pwd`,'user_email','create_time') values(?,?,?,?)");//绑定参数,引用方式传递$stmt->bindParam(1, $name, PDO::PARAM_STR); #起始值为 1 $stmt->bindParam(2, $pwd, PDO::PARAM_STR);$stmt->bindParam(3, $email, PDO::PARAM_INT);$stmt->bindParam(4, $time, PDO::PARAM_INT);// 执行预处理语句if($stmt->execute()){ echo "执行成功"; echo "最后插入的ID:".$pdo->lastInsertId();}else{ echo "执行失败!";}
PDO类 数据库连接有关(连接、执行sql)
PDO::prepare — 准备要执行的语句,并返回语句对象
PDO::query — 执行 SQL 语句,以 PDOStatement 对象形式返回结果集
PDOStatement 处理结果集 ($pdo->query()和$pdo->prepare()可以返回PDOStatement)
PDOException 异常处理类
https://www.php.net/manual/zh/class.pdostatement.php
总结:
1、query和exec都可以执行所有的sql语句,只是返回值不同而已。
2、query可以实现所有exec的功能。
3、当把select语句应用到 exec 时,总是返回 0
预处理语句(prepare)示例,sql只编译一次,执行相同的sql效率会高。单个相比exec,query效率也高。
注意:批量插入时,依次插入当遇到错误时后面的插入失败,但是前面的会插入成功。
pdo 预处理中 bindParam() 和 bindValue() 的不同之处:
PDOStatement::bindParam — 绑定一个参数到指定的变量名
PDOStatement::bindValue — 把一个值绑定到一个参数
区别就是前者使用一个php变量绑定参数,而后者使用一个值。说白了一个是变量,一个是固定值
所以使用bindParam是第二个参数只能用变量名,而不能用变量值,而bindValue至可以使用具体值。
$stm = $pdo->prepare("select * from users where user = :user"); $user = "jack"; //正确 $stm->bindParam(":user",$user); //错误 //$stm->bindParam(":user","jack"); //正确 $stm->bindValue(":user",$user); //正确 $stm->bindValue(":user","jack"); // 另外在存储过程中,bindParam可以绑定为input/output变量,如下面$stm = $pdo->prepare("call func(:param1)"); $param1 = "abcd"; $stm->bindParam(":param1",$param1); //正确 $stm->execute();
存储过程执行过后的结果可以直接反应到变量上。
对于那些内存中的大数据块参数,处于性能的考虑,应优先使用前者
下面来演示一下SQL注入:
<?php $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); # 设置结果集的默认获取的方式 (默认索引数组和关联数组) $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC); // 模拟用户输入的用户名和密码 $name = "' or 1=1 # "; $pwd = '1234'; $sql = "select user_name,user_pwd from users where user_name = '{$name}' and user_pwd = '{$pwd}'"; // 获取PDOStatement对象 $stmt = $pdo -> query($sql); echo "<pre>"; print_r($stmt); // 获取结果集中第一条数据 返回关联数组 $res = $stmt->fetchAll(); print_r($res);?>
生成的sql语句是这样的,存在严重问题
select user_name,user_pwd from users where user_name='zhang' and user_pwd='1234';# 但用户如果输入的用户名是 ' or 1=1 # select user_name,user_pwd from users where user_name='' or 1=1 #' and user_pwd='1234';
下面我们用预处理语句方式来查询看一下生成的SQL语句和执行结果
<?php $dsn = 'mysql:host=127.0.0.1;dbname=mydb'; $user = 'root'; $pwd = 'root'; $pdo = new PDO($dsn,$user,$pwd); # 设置结果集的默认获取的方式 (默认索引数组和关联数组) $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC); // 模拟用户输入的用户名和密码 $name = "' or 1=1 # "; $pwd = '1234'; $sql = "select user_name,user_pwd from users where user_name=? and user_pwd=?"; $stmt = $pdo -> prepare($sql); $stmt->execute([$name,$pwd]); echo "<pre>"; print_r($stmt); $res = $stmt->fetchAll(); print_r($res);?>
PDO预处理能防止sql注入的原因:
1、先看预处理的语法
#无序方式 命名占位符预处理$pdo->prepare('select user_name from users where user_name=:user_name');$pdo->execute([':user_name'=>'admin']); #有序方式 问号占位符预处理$pdo->prepare('select user_name from users where user_name=?');$pdo->execute(['admin']);
prepare 服务器发送一条sql给mysql服务器,mysql服务器会解析这条sql。
execute 服务器发送一条sql给mysql服务器,mysql服务器不会解析这条sql,只会把execute的参数当做纯参数赋值给语句一。哪怕参数中有sql命令也不会被执行,从而实现防治sql注入。
实战:完善登录注册功能
文件夹基本结构如下:
common.php
<?php /** * 公共模型文件 */// 连接数据库服务require "../../config/connect.php";function checkName($uname){ global $pdo; // 默认数据库中用户名不存在同名 $isOccupied = false; // 编写查询用户名SQL语句 $sql = "select user_name from users where user_name='{$uname}'"; // 返回 pdo Statement对象 $stmt = $pdo->query($sql); // 以关联数组方式返回结果集 $res = $stmt->fetchAll(PDO::FETCH_ASSOC); // 如果不想每次获取时都设置返回方式,也可以直接给pdo对象设置返回类型 // $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC); if($res){ $isOccupied = true; } return $isOccupied;}function insertData(Array $user){ global $pdo; // 获取当前时间戳 $create_time = time(); // 将密码进行加密处理 $password = password_hash($user['pwd'],PASSWORD_BCRYPT); // 准备插入的sql语句 $sql = "insert into users (user_name,user_pwd,create_time,user_email) values ('{$user['userName']}','{$password}',{$create_time},'{$user['email']}')"; // 执行插入SQL语句 $count = $pdo->exec($sql); return $count;}?>
regist_check.php
<?php // 导入公共函数库require "./common.php";// 接受用户登陆时提交的验证码session_start();// 接收PSOT传递过来的数据$user = $_POST;// 非表单提交访问页面给予提醒if(empty($user)) die('请勿非法访问!!!');// 判断请求的类型if(isset($_POST['type']) && $_POST['type']==1 ){ if(!empty($_POST["uname"])){ // 调用函数判断用户名是否被占用 $res = checkName($_POST["uname"]); if($res){ echo json_encode(['status'=>0,'msg'=>'该用户名已被占用']); }else{ echo json_encode(['status'=>1,'msg'=>'该用户名合法']); } }}else if(isset($_POST['type']) && $_POST['type']==2 ){ if(!empty($_POST["captcha"])){ if(strtolower($_SESSION["captcha"]) === strtolower($_POST["captcha"])){ echo json_encode(['status'=>1,'msg'=>'验证码正确']); }else{ echo json_encode(['status'=>0,'msg'=>'验证码不正确']); } }}else{ if(strlen($user['userName'])<4 || !preg_match("/^[A-Za-z]/i",$user['userName']) ){ echo json_encode(['status'=>0,'msg'=>'用户名长度需不小于四位且以字母开头']); }else if(strcmp($user['pwd'],$user['cpwd'])!== 0){ echo json_encode(['status'=>0,'msg'=>'两次密码输入不一致']); }else if(strcasecmp($_SESSION["captcha"],$user["captcha"])!== 0){ echo json_encode(['status'=>0,'msg'=>'验证码不正确']); }else if(checkName($user['userName'])){ echo json_encode(['status'=>0,'msg'=>'请勿重复多次提交']); }else{ // 调用添加数据函数 返回受影响行数 $count= insertData($user); if($count): echo json_encode(['status'=>1,'msg'=>'注册成功,请稍后……']); else: echo json_encode(['status'=>0,'msg'=>'注册失败~~~']); endif; }}?>
login_check.php
<?php // 连接数据库服务require "./../config/connect.php";// 接受用户登陆时提交的验证码session_start();if(empty($_POST)) die("请勿非法访问");$username = $_POST['userName'];$password = $_POST['pwd'];$captcha = $_POST['captcha'];// 预处理SQL模板$sql = "select user_name,user_pwd from users where user_name = ?";// prepare()方法-准备一条预处理语句 返回 pdo statement对象$stmt = $pdo->prepare($sql);// 绑定参数到指定的变量名$stmt->bindParam(1,$username,PDO::PARAM_STR);// 执行预处理SQL$stmt->execute();// 获取结果集$res = $stmt->fetch();// 判断查询出的数据条数if($stmt->rowCount()==0){ echo json_encode(['status'=>0,'msg'=>'该用户未注册……']); }else if(!password_verify($password,$res['user_pwd'])){ echo json_encode(['status'=>0,'msg'=>'用户名或密码不正确']); }else if(strcasecmp($_SESSION["captcha"],$captcha)!== 0){ echo json_encode(['status'=>0,'msg'=>'验证码错误……']); }else{ echo json_encode(['status'=>1,'msg'=>'登录成功……']); }?>
database.php
<?phpnamespace pdo_edu;return [ 'type'=> $type ?? 'mysql', 'host'=> $host ?? 'localhost', 'dbname'=> $dbname ?? 'mydb', 'username'=> $username ?? 'root', 'password'=> $password ?? 'root', 'charset'=> $charset ?? 'utf8mb4', 'port'=> $port ?? '3306'];
connect.php
<?php namespace pdo_connect;// 数据库配置信息$config = require __DIR__."./database.php";extract($config);$dsn = sprintf("%s:host=%s;dbname=%s",$type,$host,$dbname);try{ // 可选,设置错误提示级别为WARNING $params = [\PDO::ATTR_ERRMODE=>\PDO::ERRMODE_WARNING]; $pdo = new \PDO($dsn,$username,$password,$params);}catch(Exception $e){ die('数据库连接失败:'.$e -> getMessage());}?>
浏览效果:
浏览链接:
【PHP 面向对象】面向对象(OOP)编程之解读命名空间使用知识点归纳总结(四)
【PHP 面向对象】面向对象(OOP)编程之魔术方法实现重载知识点归纳总结(三)
【PHP 面向对象】面向对象(OOP)编程知识点归纳总结(二)
【PHP 面向对象】面向对象(OOP)编程知识点归纳总结(一)