月下博客

PHP回顾之数据库操作

PHP回顾系列目录

PHP数据库操作

web领域大名鼎鼎的LAMP/LNMP组合分别是Linux、Apache/Nginx、MySQL和PHP(极少情况为Perl)的首字母缩写,其中Linux是操作系统,Apache/Nginx是web服务器,MySQL是数据库,PHP是生产动态内容的编程语言。相对于同为web开发领域的Java和.Net,LAMP/LNMP的主要特定是全部都是免费且开源的。

从组合名字可以看出,MySQL是PHP的最佳数据库CP已经被广泛认可。可以毫不夸张的说,没有MySQL的PHP语言将黯然失色。MySQL是最流行的关系型数据库之一,由于其免费并且开源的特点,被许多公司修改移植并被广泛应用,也衍生了MariaDB,Percona等分支。MySQL能比较好的支持SQL语言标准,并支持大型的数据库,对PHP也有良好的支持。熟悉MySQL也是后端开发必备的基本功之一。

PHP的数据库拓展

与MySQL数据库交互的拓展有三个:mysql,mysqli和PDO_MySQL,对应三套不同风格的API。mysql拓展为连接MySQL4.1之前版本编写,不支持预编译和事务等新版特,存在注入风险。其在PHP 5.5中被奇异,并在PHP 7版本中被移除。MySQLi是MySQL的增强版本(i即improvement的缩写),提供了面向过程和面向对象两套风格的接口,支持事物等MySQL 4.1+版本的新特性。PDO是PHP数据抽象访问层(PHP Data Object)的缩写,为访问数据库定义了一个轻量级的一致接口。由于PDO不包含实现数据库的代码,必须为其制定一个具体的驱动,例如PDO_MySQL,PDO_OCI等。

三个拓展使用的数据驱动库有两个:mysqlnd和libmysqlclient。其中libmysqlclient是MySQL官方出品的驱动库,为外部程序访问和操作数据库提供了接口。mysqlnd是mysql native driver的缩写,由PHP官方出品,在5.3中被引入,在5.4+版本中作为默认的PHP MySQL拓展链接库。mysqlnd没有预先安装mysql的要求,使用了PHP内存管理可以更合理的使用内存,提高了性能。mysqlnd还支持通过插件的方式支持非MySQL数据库。官方建议优先使用mysqlnd而非libmysqlclient驱动。

数据库操作概览

日常程序开发中,常见的数据库操作包括数据定义、数据操作和事务控制,对应SQL中的DDL(Data Definition Language),DML(Data Manipulation Language)和TCL(Transaction Control Language)。create/alter/drop/truncate等对数据库的结构进行定义,属于数据定义语言DDL;select/insert/update/delete/call等对数据进行操作,属于数据操作语言DML;set transaction/rollback/commit等对事务进行控制,属于事务控制语言TCL。grant/revoke等数据库控制语言(Data Control Language, DCL)一般不会出现在程序开发中。

对应的数据接口可以分为两类:查询(query)和执行(exec)。查询要求返回结果供后续使用,执行一般不要求有结果(常见做法是返回影响的行数作为是否成功的标识)。

从组织结构看,数据库操作有三个要素:数据库连接、执行语句和结果集。

下文以PDO为例,示例数据库操作。

PDO操作

namespace tlanyan;

use PDO;
use PDOException;

$dns = "mysql:host=localhost;dbname=foo";
$username = "foo";
$password = "password";

// 建立数据库链接
$conn = new PDO($dns, $username, $password, [
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 设置出错时抛异常
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 设置结果字符集
$conn->exec("set NAMES 'utf8mb4'");

// 数据定义
$sql = <<<sql
create table user(
id int not null primary key auto_increment,
username varchar(32) not null unique,
password varchar(256) not null,
name varchar(32) not null,
age int not null
) engine=innodb default charset=utf8mb4;
SQL;
$conn->exec($sql);

// 数据操作
$sql = "insert into user values(null, 'tlanyan', sha1('1234password'), 'tlanyan', 20)";
$row = $conn->exec($sql);
$userId = $conn->lastInserId();
echo "new user id: $userId/n";

$sql = "select * from user limit 1";
$stmt = $conn->query($sql);
// 遍历结果集
foreach ($stmt as $record) {
   print_r($record);
}

$sql = "update user set password=sha1('abcd2345') where id=1";
$conn->exec($sql);

// 事务操作
$sql = "insert into user values(null, :username, sha1('test1234'), :name, 25)"; 
$stmt = $conn->prepare($sql);
$conn->beginTransaction();
try {
    for ($copy = 2; $copy < 5; ++ $copy) {
        $stmt->execute([
            ':username' => 'tlanyan' . $copy,
            ':name' => 'tlanyan' . $copy,
        ]);
    }
    $conn->commit();
} catch (PDOException $e) {
    echo 'error! message:', $e->getMessage(), PHP_EOL;
    $conn->rollback();
}
</sql>

以上代码包含了常见的数据库连接、数据增删查改、表结构操作和事物控制。数据库连接对象,命令语句和结果集,是数据操作的三个重要因素。

SQL注入

SQL注入是web开发中绕不过去的安全议题。发生SQL注入的本质原因是执行了动态的命令,如果执行的命令都是限定的,就会会出现SQL注入的现象。要防范SQL,关键点是使用编译好的命令(存储过程)或者预编译命令。

存储过程在数据库中定义,程序中按照指定参数call即可调用。对于动态语句的,PDO提供了预编译功能(如果数据库不支持预编译,PDO可以模拟预编译,需要设置PDO::ATTR_EMULATE_PREPARES为true)。预编译命令后,返回一个PDOStatement对象,绑定参数后即可安全的执行命令。

以下代码是PDO预编译的一个例子:

// 执行命令
$sql = "select id, name, age from user where username=:username";
// 编译命令
$stmt = $conn->prepare($sql);
// 绑定变量
$username = "tlanyan";
$stmt->bindParam(":username", $username, PDO::PARAM_STR);
// 执行和获取结果
$stmt->execute();
$row = $stmt->fetch();
print_r($row);

// 变量变量后,可方便的改变变量的值,多次执行预编译命令获取结果
$username = 'tlanyan2'
$stmt->execute();
print_r($stmt->fetch());

杂项