本节书摘来自华章社区《PHP精粹:编写高效PHP代码》一书中的第2章,第2.4节PHP数据库对象,作者:(美) Davey Shafik,更多章节内容可以访问云栖社区“华章社区”公众号查看
2.4 PHP数据库对象如果之前你使用过PHP和MySQL,你可能用过mysql或mysqli类库连接到数据库,如使用mysql_connect()函数。多年来,这是连接到MySQL数据库的标准方式,并且对于其他数据库平台也使用同样的方式。这些类库可以直接使用,并形成了无数PHP应用程序类库和框架的基础。这种方式的缺点是每个扩展都与其他稍有不同,因此使得代码在数据库平台之间轻松转移变得复杂。虽然这些数据库特定类库依然活跃并且运转良好,但是本章中仍将专注讲解更先进的PDO扩展。创建的PDO扩展提供了一组统一功能与各种数据库平台的对话。PHP 5采用的就是面向对象的扩展,当时人们将它的很多特性引入了PHP语言。理解OOP如果你不熟悉面向对象编码,并且已仔细阅读第1章,现在你正好可以了解使用OOP的更多内容。
然而,PDO并没有解决问题,这个问题是出现在不同数据库平台之间的SQL语法差异;因此,乍看起来似乎这个扩展并不完全是最有效的手段。PDO可以和各种各样的数据库平台连接和对话,但是为了创建真正独立于平台的应用程序,我们不得不改写发送的SQL语句。PDO是一个抽象层,这表明它建立在PHP以及PHP连接数据库的方式之间。PDO提供了非常简洁的功能来执行查询和遍历数据集,让我们深入研究如何使用PDO的技术细节吧。
2.4.1 使用PDO连接到MySQL要使用PDO连接到数据库,需要实例化一个新的PDO对象并且传递一个DSN,如果需要,还要加上用户名和密码。DSN(Data Source Name, 数据源名称)由描述实际连接的数据结构组成。若要连接到创建(数据库名称recipes,使用localhost作为主机名)的数据库,将使用下面的PHP代码生成连接:
2.4.2 从表中选择数据创建了PDO对象之后,就可以检索数据了。首先,在数据库中会有怎样的食谱清单呢?当用PDO选择数据时,要创建一个PDOStatement对象。它代表查询,并使我们获取结果。对于一个基本的查询,可以使用PDO::query()方法:
使用ORDER对结果排序当我们像这样从MySQL中选择数据时,将得到以未定义的顺序返回的记录;通常这些记录是按照插入的顺序排序的。为成为更完美的应用,可以在查询的末尾添加这样的命令:ORDER BY created DESC。将按时间的降序返回结果,即总是先看到最新的食谱。
以上示例利用了PDOStatement::fetch()方法,此方法能处理大量提取数据的模式。2.4.3 数据提取模式在前面的示例中,我们看到了如何用PDOStatement对象来表示查询及其数据集。每次调用fetch()方法,都将从结果集中接收到另外一行。还可以使用fetchAll()方法一次检索所有的行。这两种方法都接受fetch_style参数,这个参数定义如何格式化结果集。PDO提供了便于使用的常量: PDO::FETCH_ASSOC完成了以前你在while循环中看到的,它使用键组返回数组到列名。 PDO::FETCH_NUM也返回数组,但这次使用数字键。 PDO::FETCH_BOTH(默认值)结合了PDO::FETCH_ASSOC和PDO::FETCH_NUM以提供一个每个值出现两次的数组,一次使用其列名,一次使用数字索引。 PDO::FETCH_CLASS返回一个已命名的类的对象而不是数组,这些值以列的名字命名设置到属性中。为了看到由PDO::FETCH_ASSOC返回的结果,可以输入下列代码:
这里有一些正在运行,让我们依次看看它们。首先,通过传递SQL语句进入prepare()方法创建了PDOStatement。仔细观察这条SQL语句,你可能会看到一些奇怪的东西。在:recipe_id前面的冒号表示这是一个占位符(placeholder)。在实际运行这个查询之前,会用真正的值来替换这个占位符。然后,execute()这个查询。必须为字符串中的每个占位符传入值,而且还要将这些字符串传入prepare()方法中。因为使用指定的占位符,所以要创建一个由与这些占位符数量相同的元素组成的数组。每个占位符都有一个与之匹配的数组元素,数组元素的名字作为键值,然后要用它的实际值来替换键值。既然已知道只能返回一行,可以通过调用一次fetch()方法来代替循环。
生成SQL语句在之前的示例中,定义了一个单独的$sql变量来保存这个字符串并传入PDO::prepare。这种方法可以让代码更易于阅读,并且在需要建立一个更复杂的查询时提供帮助。这种方式也可以帮助我们进行调试,你可以不费力地检查有哪些东西传入了prepare()。
占位符不需要名字,你也可以使用??符号为变量保留一个位置作为没有命名的占位符。此外,在SQL语句中有很多这样的占位符,用它们来创建PDOStatement,而且作为数组把这些值传入execute()中,但在这个例子中,我们必须将这些值按顺序排列在查询语句中。用下面的示例很容易说明这些:如果查询变得很庞大或很复杂,命名占位符可使你更容易保存代码。将数组中命名的键值传入execute(),比起应付一个巨大的用数字作为索引的数组,这种方式让你更容易看出哪个值属于哪个参数。预处理语句使我们清楚地标识出查询中哪些部分是数据库语言,哪些包含可变数据。你会听说“安全咒语”:“过滤输入,避免输出”(如果你还未听说,在第5章将很快看到)。当使用数据库时,必须溢出已经发送到数据库的值(也就是说删除不需要的字符)。你可能见过像mysql_escape_string()这样的MySQL功能。当使用预处理语句时,为占位符传入的值已经溢出,因为MySQL知道这些都是可能改变的值。这种额外的安全保障是将PDO及预处理语句作为规范令人信服的一个原因。
2.4.5 绑定值和预处理语句的变量既然MySQL已经准备了一个查询,那么使用不同的值再次运行这个查询时只会有很小的系统开销。我们已经知道了如何传递变量到PDOStatement的execute()方法中。在本节中,我们将看到如何绑定值甚至变量到语句中,以便在每次执行查询时都使用这些值或变量。解释概念的简单例子这些例子可能看起来确实没有什么意义,但举例说明数据集里更多先进的技术的确是种乐趣!如果你问自己:“为什么我要尝试这些呢?”要记住这些都是适用于你自己项目的技术(可能用在更复杂的设置中)。
虽然这是事实,但总的来说,我们最好用尽可能少的步骤从数据库中检索数据,有时你使用的查询类型意味着这些步骤不能组合使用。当使用不同的值重复调用相同的查询时,可以设置一些元素用于每次查询。例如,如果我们总想使用同样的chef值,可以使用PDOStatement::bindValue():
最后这两个例子表明了在调用execute()方法之前如何设定值或变量到PDOStatement对象。不管你是使用bindValue()方法、bindParam()方法或是传入值到execute()方法本身,预处理语句都是极为有用的!如果我们多次运行这个语句,这种方式不仅能够提高代码的性能,而且也可以毫无疑问地溢出占位符。
2.4.6 插入一行并获取ID前面已经深入研究了SELECT语句的选项,但是INSERT和UPDATE语句又怎么样呢?实际上这几个语句看起来确实很相似,即我们预处理然后再执行一条语句。接下来以插入一些新的食谱作为示例:
rowCount()是PDOStatement对象的一个方法,它会指出有多少行由于查询而发生改变。2.4.8 删除数据和插入或更新数据一样,我们以相同的方式删除数据,即对查询进行预处理然后再执行查询。如果想删除“Starter”这一类别(因为它未使用),可以这样做:
我们会再次使用rowCount()方法检查删除行,查看其数量是否和预期的一样多(很多缺失的或错误的WHERE子句比预期会造成更大损害)。