使用 PDO 创建数据访问对象
数据访问对象(DAO)是一种将数据从数据库或API中取出并在整个应用程序中以统一方式呈现的方法。作为一种设计模式,它用于标准化特定数据位的传递方式,而不必求助于使用数组来完成相同的工作。
前几天我正在使用PHP的PDO库做一些工作,当时我注意到我可以使用fetchAll()带有PDO::FETCH_CLASS标志的方法从数据库中获取数据。此标志自动返回对象中的数据。这让我更多地思考这是如何工作的,但我找不到很多谈论这个的文章。因此这篇文章。
设置事情
让我们来看看一个叫做SpecialText的类,它将存储一个ID和一些文本,以及一个__toString()允许打印这些值的方法。我们将在整篇文章中使用这个类来存储数据库中的数据。
class SpecialText { protected $id; protected $specialtext; public function __toString() { $output = self::CLASS . PHP_EOL; $output .= 'ID: ' . $this->id . PHP_EOL; $output .= 'Special Text: ' . $this->specialtext . PHP_EOL; return $output; } }
我们还需要一个数据库。要创建的最简单的数据库是使用sqlite。下面将在与PHP脚本相同的目录中创建sqlite数据库文件,并创建一个名为specialtexttable的表。
$databaseFile = realpath(__DIR__) . '/testdatabase.sqlite'; $databaseHandle = new \PDO('sqlite:' . $databaseFile, '', '', array(\PDO::ATTR_PERSISTENT => false)); $sql = 'DROP TABLE IF EXISTS "specialtexttable"; CREATE TABLE "specialtext" ( "id" INTEGER, "specialtext" TEXT NOT NULL, PRIMARY KEY("id" AUTOINCREMENT) );'; $databaseHandle->exec($sql);
接下来,我们需要向表中插入一些数据。一个简单的插入就可以解决这个问题。
$sql = 'INSERT INTO specialtexttable(specialtext) VALUES (:specialtext);'; $query = $databaseHandle->prepare($sql); $query->execute([':specialtext' => 'text text text']);
现在我们准备从数据库中提取数据。
按索引列获取
默认情况下,该fetchAll()方法将返回一个包含索引项和关联项组合的数组。这意味着我们的两个值表将包含在一个包含四个值的数组中。更常见的是传入\PDO::FETCH_ASSOC以便PDO将根据结果创建一个关联的值数组。下面的示例使用该fetchAll()方法从数据库返回一个关联数组。
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->execute(); $result = $query->fetchAll(\PDO::FETCH_ASSOC); print_r($result);
请注意,该fetch()方法本质上等同于该fetchAll()方法。不同之处在于该fetch()方法将返回单个结果,如果没有更多结果,fetchAll()则返回false,并且将始终返回一个项目数组。
上面的代码打印出以下内容。
Array ( [0] => Array ( [id] => 1 [specialtext] => text text text ) )
通过stdClass获取
如果您将标志\PDO::FETCH_OBJ或\PDO::FETCH_CLASS传递给该fetchAll()方法,而没有任何其他参数,那么您将返回stdClass对象的一个实例,该对象将列名称映射到类中的属性。
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->execute(); $objects = $query->fetchAll(\PDO::FETCH_CLASS); print_r($objects);
这将打印出以下内容。
Array ( [0] => stdClass Object ( [id] => 1 [specialtext] => text text text ) )
虽然这是可能的,但我们实际得到的只是一个数组。最好不要使用stdClass对象,除非您真的必须这样做,因为您无法从使用完整PHP类获得的任何正常控件或功能中受益。让我们通过使用一个类来使它更有用。
按类获取
The\PDO::FETCH_CLASSflagwillallowthefetchAll()methodtoinjectvaluesintoaclassthatwespecify,callingtheconstructorafterwards.ThesecondparameteroffetchAll()isthenameoftheclassthatyouwanttouse.YoumayhavenoticedthattheSpecialTextclasscontainsprotectedproperties,butthisdoesnotmatterasPDOwillstillreturnanobjectthatmapsthecolumnnamestoyourpropertynames.YoucanevensettheclasspropertiesasprivateandPDOwouldstillreturnavalidobject.PDOwillessentiallybypasstheconstructoranddirectlyinjectpropertiesintoyourobject.
以下是如何将SpecialText类与fetchAll().第二个参数必须是字符串,因此您也可以传入“SpecialText”,但我发现使用特殊的::class常量可以在涉及命名空间时提高代码可读性。
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->execute(); $objects = $query->fetchAll(\PDO::FETCH_CLASS, SpecialText::class); foreach ($objects as $object) { echo $object; }
这将打印出以下内容。
SpecialText ID: 1 Special Text: text text text
该SpecialText对象现在可以用作任何其他对象。
重要的是要记住,类的属性名称必须与表的列名称完全匹配才能使其正常工作。如果您的类属性与表名不同(即使是大写),那么PDO将创建一个具有包含该值的附加属性的对象。例如,如果我将specialtext属性更改为specialText,则结果对象将包含一个名为specialText的空属性和一个名为specialtext的属性,该属性包含来自数据库的值。
可以通过向__set()类添加方法来覆盖此功能。如果设置了类的不存在属性,则始终调用此魔术方法。然后,您可以拦截呼叫并采取相应的行动。
构造函数顺序
如果\PDO::FETCH_CLASS标志与\PDO::FETCH_PROPS_LATE结合使用,则在将属性添加到类之前调用构造函数。这使得对象实例化的行为更像平常。孤立的不是很清楚这里有什么变化,所以让我们修改SpecialText类以包含一个标志,用于何时使用\PDO::FETCH_PROPS_LATE。如果在运行构造函数时specialtext属性为null,则这是一个用字符串填充的属性。
class SpecialText { protected $id; protected $specialtext; protected $propsLate; public function __construct() { if (is_null($this->specialtext)) { $this->propsLate = 'late props used'; } } public function __toString() { $output = self::CLASS . PHP_EOL; $output .= 'ID: ' . $this->id . PHP_EOL; $output .= 'Special Text: ' . $this->specialtext . PHP_EOL; $output .= 'FETCH_PROPS_LATE: ' . $this->propsLate . PHP_EOL; return $output; } }
然后我们fetchAll()并排运行两个不同的变体。
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->execute(); $objects = $query->fetchAll(\PDO::FETCH_CLASS, SpecialText::class); foreach ($objects as $object) { echo $object; } $query = $databaseHandle->prepare($sql); $query->execute(); $objects = $query->fetchAll(\PDO::FETCH_CLASS|\PDO::FETCH_PROPS_LATE, SpecialText::class); foreach ($objects as $object) { echo $object; }
比较上面的输出,可以清楚地看到,如果没有\PDO::FETCH_PROPS_LATE,propsLate属性没有被填充,但是当该标志存在时,该属性包含字符串。
SpecialText ID: 1 Special Text: text text text FETCH_PROPS_LATE: SpecialText ID: 1 Special Text: text text text FETCH_PROPS_LATE: late props used
指定构造函数参数
最后,还可以向构造函数提供参数。让我们修改SpecialText类以包含一个构造函数。
class SpecialText { private $id; protected $specialtext; public function __construct($id, $specialtext) { $this->id = $id; $this->specialtext = $specialtext; } public function __toString() { $output = self::CLASS . PHP_EOL; $output .= 'ID: ' . $this->id . PHP_EOL; $output .= 'Special Text: ' . $this->specialtext . PHP_EOL; return $output; } }
应用与fetchAll()我们上面使用的相同的代码将出现以下错误。
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function SpecialText::__construct(), 0 passed and exactly 2 expected in pdo.php:27 Stack trace: #0 [internal function]: SpecialText->__construct() #1 pdo.php(106): PDOStatement->fetchAll(1048584, 'SpecialText') #2 {main} thrown inpdo.phpon line 27
这是因为构造函数仍在运行,并且没有传递任何参数,结果是致命错误。
为了解决这个问题,我们可以添加第三个参数,它是要发送到构造函数的默认值。下面的示例将两个构造函数值都设置为NULL,但由于\PDO::FETCH_PROPS_LATE值正在使用中,这些值很快就会被实际值覆盖。
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->execute(); $objects = $query->fetchAll(\PDO::FETCH_CLASS|\PDO::FETCH_PROPS_LATE, SpecialText::class, [NULL, NULL]); foreach ($objects as $object) { echo $object; }
这与上面两个\PDO::FETCH_CLASS调用的输出相同。
我看到有人说数组的参数是数据库中的字段名,这是不正确的,因为数组的值直接传递给构造函数。这绝对不是要映射到构造函数的字段名称数组。
设置获取模式
fetchAll()始终需要将完整的参数列表发送到。另一种方法是在运行该fetchAll()方法之前设置提取模式。
这是与前面的示例等效的示例,但在此示例中,我们fetchAll()使用
$sql = "SELECT * FROM specialtexttable;"; $query = $databaseHandle->prepare($sql); $query->setFetchMode(\PDO::FETCH_CLASS|\PDO::FETCH_PROPS_LATE, SpecialText::class, [null, null]); $query->execute(); $objects = $query->fetchAll();
结论
虽然可以通过这种方式使用\PDO::FETCH_CLASS,但我可能不会在任何项目工作中使用它。我在文章开头谈论的工作只是我正在玩的一个小爱好项目,需要一种将数据快速存储在数据库中的方法。我在网上看了一下,并没有看到这种技术在开源项目中被大量使用。
这里的一个问题是类中的属性名称必须与数据库表中的字段名称完全相同。这意味着您的DOA对象及其属性固有地绑定到您的数据库中。这意味着您在命名字段和属性时必须特别小心,因为如果您之后需要重命名,您将需要进行大量更改。通过使用魔术__set()方法,您可以潜在地最小化将对象属性和字段名称绑定在一起的影响。
以这种方式运行fetch操作确实减少了编写代码的时间,但是你所做的是隐藏魔法属性填充和难以调试的代码。你有没有用过这种技术?在评论中告诉我。