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 处理结果集 ( p d o − > q u e r y ( ) 和 pdo->query()和 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
<?php
namespace 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());
}
?>
浏览效果:
浏览链接:
-
登录:http://www.zhsh520.com/admin/login/login.html
-
注册:http://www.zhsh520.com/admin/regist/regist.html
转载:https://blog.csdn.net/weixin_43958049/article/details/116759167