PHP 数据库拓展之 PDO

PDO

js经常操作dom元素,相比,php最常操作的就是跟数据库的交互,我们写很多逻辑,经常都是需要读写数据,所以在php访问mysql,已经有了三个成熟的拓展,分别是pdo,mysqli,mysql,但是mysql在php7已经被移除了;

pdo是一个数据抽象层,并没有实现和数据库的交互,而是统一定义了一套方法,具体的操作数据功能需要引入pdo数据库驱动来实现;当我们需要和mysql交互的时候,就引入pdo_mysql驱动,当我们需要和sqlite交互的时候,就引入pdo_sqlite驱动;这就是他和mysqli最大的不同,mysqli是只针对mysql数据库而开发出来的拓展,而pdo可以兼容多个数据库,灵活性比较强,但是性能会比mysqli差一点。laravel框架数据库底层也是对pdo进行封装。

安装和配置

从php5.1起pdo和pdo_sqlite拓展是默认安装的,但是我们经常操作的数据库是mysql,所以我们第一步就是学习如何安装pdo_mysql拓展 驱动

  • 在源码安装的时候,捎上pdo_mysql
    首先,我们应该掌握php源码安装的方法,如果不清楚的同学,可以去先去学习下,我们都知道源码安装的过程需要执行一步 ./configure --prefix=/usr/local/php 来生成编译文件,在这里我就只指定了一个prefix参数,来指定php的安装路径,所以在这里 我们可以加上参数 --with-pdo-mysql参数,指定在安装php的时候,pdo_mysql拓展驱动也加载进来
    PHP 数据库拓展之 PDO
    编译安装后,我们可以通过php -m或者php -i | grep pdo_mysql ,或者如果你配好ngixn和php交互的web服务,通过执行 php文件中的方法phpinfo()来查看是否加载成功

  • 如果我们是在安装完php后,想加载pdo_mysql拓展的话,可以通过源码包里面有个专门放置拓展文件源码的地方,进行编译安装,或者通过pecl install pdo_mysql 安装,也可以到https://pecl.php.net/package/PDO_MYSQL将源码下载下来,然后再进行编译安装,具体安装拓展的方式,不懂的同学可以去学习下。这里就不详解了。需要注意一点是,这种安装方式,需要在编译安装好拓展后,在php的ini配置文件,加上拓展 extension=pdo_mysql,然后重启php进程,如果是命令行执行的话,就不需要重启,实时加载配置。

pdo配置的话,基本上没啥配置,在pdo底层源码里,主要有几个类:PDO、PDOException、PDOStatement。其中PDOException主要是连接数据库错误时和设置PDO错误模式时会抛出异常,重点是学习PDO和PDOStatement里面的方法如何运用,至于PDO里面有大一堆常量,有兴趣的同学可以了解下

使用

连接数据库

当我们了解完PDO和安装完拓展之后,我们第一步就是连接上mysql数据库,我们知道mysql的架构模式是C/S,客户端和服务端交互,我们通常连接mysql是通过,mysql -h 127.0.0.1 -u root -proot 来连接数据库,这是mysql本身自带的客户端。所以pdo_mysql驱动拓展就是用来代替mysql客户端,实现与mysql的交互。

$dsn = 'mysql:host=127.0.0.1;dbname=test';
$user = 'root';
$pass = 'root';
$dbh = new PDO($dsn, $user, $pass);

上面就是简单的连接,在dsn数据源名称中我们指定了使用pdo_mysql驱动,主机和数据库名称,其次就是用户和密码,当我们连接成功的时候,会返回一个pdo对象;失败的时候,则会抛出一个PDOException异常,如果我们不捕捉异常的话,就有可能泄露数据库用户和密码;
PHP 数据库拓展之 PDO
所以我们通常都会捕捉这个异常

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root ';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('数据库连接失败');
}

当我们new一个pdo对象的时候,这个对象就是一个数据库连接句柄,如果对这个对象进行多次引用的话,数据库连接还是原来这个,同理,只有当这个对象的所有引用都被销毁的时候,这个数据库连接才会断开。怎么验证这个原理呢?mysql中有一个show processlist 命令来展示现在当前mysql服务器上的所有线程连接池

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root ';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('数据库连接失败');
}
sleep(15);
file_put_contents('./test.log','开始引用对象了');
$dbh1 = $dbh;
$dbh2 = $dbh;
sleep(15);

在执行上面那段代码的时候,先使用 show processlist查看当前mysql服务器上面的线程连接池
PHP 数据库拓展之 PDO
然后执行代码,卡在15秒之前赶紧执行一次 show processlist
PHP 数据库拓展之 PDO
发现多了一个数据库连接,没错,就是第一次数据库连接,等到日志产生了开始引用对象了内容的时候,再执行一次 show processlist
PHP 数据库拓展之 PDO
发现并没有产生新的数据库连接句柄,因为我们只是对对象进行引用,指向的都是同一个数据库连接。所以要断开数据库连接很简单,把这个对象的所有引用主动的销毁或者脚本执行结束的时候会自动销毁掉。
PHP 数据库拓展之 PDO
那么问题来了?实际中,在很多地方,比如各种方法都需要操作数据,就会new一个数据库连接句柄,如果在一个线程中,很多地方都new一个新的数据库对象的话,那么就会产生很多的连接,造成大量资源消耗,并且这是不可控的。

$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root';
$pass = 'root';
try {    
$dbh0 = new PDO($dsn, $user, $pass);    
$dbh1 = new PDO($dsn, $user, $pass);    
$dbh2 = new PDO($dsn, $user, $pass);    
$dbh3 = new PDO($dsn, $user, $pass);    
$dbh4 = new PDO($dsn, $user, $pass);    
$dbh5 = new PDO($dsn, $user, $pass);    
$dbh6 = new PDO($dsn, $user, $pass);    
$dbh7 = new PDO($dsn, $user, $pass);    
$dbh8 = new PDO($dsn, $user, $pass);    
$dbh9 = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('数据库连接失败');
}
sleep(30);

在执行上面那个脚本之前,使用 show processlist查看当前连接线程池
PHP 数据库拓展之 PDO
执行脚本后,再次执行show processlist
PHP 数据库拓展之 PDO
产生了10个数据库连接,那么解决办法就是使用单例模式,使在一个线程或者请求中,数据库的连接只会产生一个,具体可以看后面,我使用了pdo,运用单例模式封装了一个mysql类。

事务

在学习使用pdo类其他方法前,先学习下pdo类中的事务相关的四个方法:beginTransaction()、commit()、rollBack()、inTransaction(),那么首先我们应该先了解mysql事务,以及这四个方法代表mysql哪条命令;事务支持四大特性(ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)以及持久性(Durability),一个事务的特点就是原子性,要么都执行成功,要么都失败。事务在实际应用中非常重要,可以避免异常数据的产生,导致数据不一致。具体详细的可以自行去学习mysql的事务原理。
方法映射mysql命令
beginTransaction():start transaction
commit():commit
rollBack():rollback

// 事务都会跟php的抛异常结合,抛异常通常也是解决流程一致性问题
$dsn = 'mysql:host=172.17.0.3;dbname=laravel';
$user = 'root';
$pass = 'root';
try {    
$dbh = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {    
die('数据库连接失败');
}
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh->beginTransaction();
try {    
$dbh->exec('insert into `user` (`name`, `age`) value("Test", 27)');    
$dbh->exec('insert into `address` (`user_id`, `city`) value("Test", 
"SZ")'); // user_id列是int类型,插入了字符串会报错    
$dbh->commit();
} catch (PDOException $e) {   
$dbh->rollBack();    
die($e->getMessage());
}

报错:
SQLSTATE[HY000]: General error: 1366 Incorrect integer value: 'Test' for column 'user_id' at row 1
然后去查下数据库用户并没有插入进去,所以该事务被回滚了。保证了数据一致性。

预处理语句

平常我们都在写业务,很多人没去考虑安全性和效率问题。去执行一条sql的时候,可能更多的人会直接选择pdo类中的query()、exec()读写两个操作;而不会采用prepare()方法来产生一个预处理语句对象,然后再进行绑定参数,再执行sql。我们先看下两种方法都是怎么用的。

    ##  非预处理
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
        $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {   
        die('数据库连接失败');
    }
    $id = $_GET['id'];
    $data = $dbh->query('select * from `users` where `id` > '. $id);
    foreach ($data as $user) {    
        echo $user['id'] . ':' . $user['name'] . '<br/>';
    }

    ##  预处理语句
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $stmt = $dbh->prepare('select * from `users` where `id` > :id');
    $stmt->bindParam(':id', $id);
    $id = $_GET['id'];
    $stmt->execute();
    while ($row = $stmt->fetchObject()) {   
    echo $row->id . ':'. $row->name . '<br/>';
    }

这两种方式执行的效率是差不多的

  • 如果我们传入参数 id=1;truncate table address;这个时候非预处理语句会把address表数据给清空了,但是预处理语句则可以不会;这就是预处理语句可以防止sql注入,当然非预处理语句,如果参数是C端(用户)输入的话,我们则可以使用正则等工具先过滤参数,也可以解决这个sql注入问题。

  • 如果对mysql比较有研究的,会知道一条sql的执行过程是怎么样的?首先当客户端向mysql服务器发送一条sql的时候,会先经过查询缓存,如果是之前查过的,则直接返回结果;否则,sql会被解析成mysql一种特有的数据结构,叫做解析树,然后再通过优化器优化,最后调用存储引擎api执行sql;如果一条比较复杂的sql,经过解析是会消耗比较多资源和时间,那么问题来了?如果一条语句,只是其中的几个参数不一样或者在并发执行的时候,如果不采用预处理语句,则需要反复进行解析,给mysql带来资源消耗,同时加重web服务器的负载。

错误处理模式

pdo 提供了三种不同的错误处理模式,以满足不同风格的应用开发,注意点:不管当前是否设置了 PDO::ATTR_ERRMODE ,如果连接失败,PDO::__construct() 将总是抛出一个 PDOException 异常。未捕获异常是致命的。

  1. PDO::ERRMODE_SILENT
    此为默认模式。 pdo 将只简单地设置错误码,可使用 PDO::errorCode() 和 PDO::errorInfo() 方法来检查语句和数据库对象
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->query('select * from `test`');
    print_r($dbh->errorCode());
    echo '<br/>';
    print_r($dbh->errorInfo());

    PHP 数据库拓展之 PDO

  2. PDO::ERRMODE_WARNING
    除设置错误码之外,PDO 还将发出一条传统的 E_WARNING 信息。如果只是想看看发生了什么问题且不中断应用程序的流程,那么此设置在调试/测试期间非常有用。
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING]);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->query('select * from `test`');
    print_r($dbh->errorCode());
    echo '<br/>';
    print_r($dbh->errorInfo());

    PHP 数据库拓展之 PDO

  3. PDO::ERRMODE_EXCEPTION
    除设置错误码之外,PDO 还将抛出一个 PDOException 异常类并设置它的属性来反射错误码和错误信息。此设置在调试期间也非常有用,因为它会有效地放大脚本中产生错误的点,从而可以非常快速地指出代码中有问题的潜在区域(记住:如果异常导致脚本终止,则事务被自动回滚)。
    异常模式另一个非常有用的是,相比传统 PHP 风格的警告,可以更清晰地构建自己的错误处理,而且比起静默模式和显式地检查每种数据库调用的返回值,异常模式需要的代码/嵌套更少。
    $dsn = 'mysql:host=172.17.0.4;dbname=test';
    $user = 'root';
    $pass = 'root';
    try {    
    $dbh = new PDO($dsn, $user, $pass);
    } catch (PDOException $e) {    
    echo 'Connect error:'. $e->getMessage();    
    die;
    }
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->query('select * from `test`');

    PHP 数据库拓展之 PDO

    使用PDO封装mysql

github

地址:https://github.com/ZengJiangBin/Mysql
代码会慢慢更新总结,也希望大神们可以给出更多的意见和留言

php
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

:smirk: 我来凑个热闹

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!