Skip to content

Latest commit

 

History

History
264 lines (209 loc) · 10.7 KB

jiang-shi-ti-lei-yu-rdbms-cha-xun-bang-ding.md

File metadata and controls

264 lines (209 loc) · 10.7 KB

将实体类与RDBMS查询绑定

大多数商业上可行的RDBMS系统都是在程序化编程处于领先地位的时候发展起来的。想象一下,RDBMS世界是二维的、方形的、面向程序的。相反,实体可以被认为是圆形的、三维的、面向对象的。这让你了解到我们想通过将RDBMS查询的结果绑定到实体实例的迭代中来实现什么。

{% hint style="info" %} 现代RDBMS系统所基于的关系模型是由数学家Edgar F. Codd在1969年首次描述的。第一套商业化的系统是在70年代中后期发展起来的。所以,换句话说,RDBMS技术已经有40多年的历史了! {% endhint %}

如何做...

1.首先,我们需要设计一个类来存放我们的查询逻辑。如果你遵循的是领域模型,这个类可能被称为仓库。另外,为了保持简单和通用,我们可以简单地调用新类Application\Database\CustomerService。该类将接受一个Application\Database\Connection实例作为参数。

namespace Application\Database;

use Application\Entity\Customer;

class CustomerService
{
    
    protected $connection;
    
    public function __construct(Connection $connection)
    {
      $this->connection = $connection;
    }

}

2.现在,我们将定义一个fetchById()方法,它以客户ID作为参数,并返回一个单一的Application\Entity\Customer实例或失败时返回boolean FALSE。乍一看,简单地使用PDOStatement::fetchObject()并指定实体类作为参数似乎是不费吹灰之力。

public function fetchById($id)
{
  $stmt = $this->connection->pdo
               ->prepare(Finder::select('customer')
               ->where('id = :id')::getSql());
  $stmt->execute(['id' => (int) $id]);
  return $stmt->fetchObject('Application\Entity\Customer');
}

{% hint style="info" %} 然而,这里的危险是,fetchObject()实际上在调用构造函数之前就已经填充了属性(即使它们是受保护的)!相应地,构造函数有可能意外地覆盖值。相应地,构造函数有可能意外地覆盖值。如果你没有定义一个构造函数,或者你可以忍受这种危险,我们就可以了。否则,要正确地实现RDBMS查询和OOP结果之间的联系就开始变得艰难了。 {% endhint %}

3.fetchById()方法的另一种方法是先创建对象实例,从而运行其构造函数,并将获取模式设置为PDO::FETCH_INTO,如下例所示。

public function fetchById($id)
{
  $stmt = $this->connection->pdo
               ->prepare(Finder::select('customer')
               ->where('id = :id')::getSql());
  $stmt->execute(['id' => (int) $id]);
  $stmt->setFetchMode(PDO::FETCH_INTO, new Customer());
  return $stmt->fetch();
}

4.然而,在这里我们又遇到了一个问题:fetch()fetchObject()不同,不能覆盖受保护的属性;如果尝试的话,会产生以下错误信息。这意味着我们要么将所有属性定义为public,要么考虑另一种方法。

  1. 我们将考虑的最后一种方法是以数组的形式获取结果,并手动给实体注入。尽管这种方法在性能上成本略高,但它允许任何潜在的实体构造函数正常运行,并将属性安全地定义为私有或保护。
public function fetchById($id)
{
  $stmt = $this->connection->pdo
               ->prepare(Finder::select('customer')
               ->where('id = :id')::getSql());
  $stmt->execute(['id' => (int) $id]);
  return Customer::arrayToEntity(
    $stmt->fetch(PDO::FETCH_ASSOC));
}
  1. 为了处理一个产生多个结果的查询,我们需要做的就是产生一个填充实体对象的迭代。在这个例子中,我们实现了一个fetchByLevel()方法,它以Application\Entity\Customer实例的形式,返回给定级别的所有客户。
public function fetchByLevel($level)
{
  $stmt = $this->connection->pdo->prepare(
            Finder::select('customer')
            ->where('level = :level')::getSql());
  $stmt->execute(['level' => $level]);
  while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    yield Customer::arrayToEntity($row, new Customer());
  }
}
  1. 我们希望实现的下一个方法是save()。然而,在我们继续之前,必须考虑到如果发生INSERT,将返回什么值。

  2. 通常情况下,我们会在INSERT之后返回新完成的实体类,但有一个方便的PDO::lastInsertId()方法。有一个方便的PDO::lastInsertId()方法,乍一看,似乎可以做到这一点。然而,进一步阅读文档后发现,并不是所有的数据库扩展都支持这个功能,而且支持的扩展在实现上也不一致。因此,除了$id之外,最好是有一个唯一的列,可以用来唯一地识别新客户。

  3. 在这个例子中,我们选择了电子邮件列,因此需要实现一个fetchByEmail()服务方法。

public function fetchByEmail($email)
{
  $stmt = $this->connection->pdo->prepare(
    Finder::select('customer')
    ->where('email = :email')::getSql());
  $stmt->execute(['email' => $email]);
  return Customer::arrayToEntity(
    $stmt->fetch(PDO::FETCH_ASSOC), new Customer());
}

10.现在我们准备定义save()方法。我们将不区分INSERTUPDATE,而是在ID已经存在的情况下,将该方法架构为更新,否则就进行插入。

11.首先,我们定义了一个基本的save()方法,它接受一个Customer实体作为参数,并使用fetchById()来确定这个条目是否已经存在。如果存在,我们调用doUpdate()更新方法;否则,我们调用doInsert()插入方法。

public function save(Customer $cust)
{
  // 检查客户ID> 0是否存在
  if ($cust->getId() && $this->fetchById($cust->getId())) {
    return $this->doUpdate($cust);
  } else {
    return $this->doInsert($cust);
  }
}

12.接下来,我们定义doUpdate(),它将Customer实体对象的属性拉到一个数组中,建立一个初始SQL语句,并调用flush()方法,将数据推送到数据库中。我们不希望ID字段被更新,因为它是主键。同时我们还需要指定更新哪条记录,也就是附加一个WHERE子句。

protected function doUpdate($cust)
{
  // 以数组形式获取属性
  $values = $cust->entityToArray();
  // 建立SQL语句
  $update = 'UPDATE ' . $cust::TABLE_NAME;
  $where = ' WHERE id = ' . $cust->getId();
  // 未设置ID,因为我们不想更新它
  unset($values['id']);
  return $this->flush($update, $values, $where);
}

13.doInsert()方法也是类似的,只是初始SQL需要以INSERT INTO...开头,并且id数组元素需要取消设置。后者的原因是,我们希望这个属性是由数据库自动生成的。如果成功的话,我们使用我们新定义的fetchByEmail()方法来查找新客户,并返回一个完成的实例。

protected function doInsert($cust)
{
  $values = $cust->entityToArray();
  $email  = $cust->getEmail();
  unset($values['id']);
  $insert = 'INSERT INTO ' . $cust::TABLE_NAME . ' ';
  if ($this->flush($insert, $values)) {
    return $this->fetchByEmail($email);
  } else {
    return FALSE;
  }
}

14.最后,我们可以定义flush(),它完成实际的准备和执行。

protected function flush($sql, $values, $where = '')
{
  $sql .=  ' SET ';
  foreach ($values as $column => $value) {
    $sql .= $column . ' = :' . $column . ',';
  }
  // 去掉尾部的','
  $sql     = substr($sql, 0, -1) . $where;
  $success = FALSE;
  try {
    $stmt = $this->connection->pdo->prepare($sql);
    $stmt->execute($values);
    $success = TRUE;
  } catch (PDOException $e) {
    error_log(__METHOD__ . ':' . __LINE__ . ':' 
    . $e->getMessage());
    $success = FALSE;
  } catch (Throwable $e) {
    error_log(__METHOD__ . ':' . __LINE__ . ':' 
    . $e->getMessage());
    $success = FALSE;
  }
  return $success;
}

15.为了结束讨论,我们需要定义一个remove()方法,从数据库中删除一个客户。同样,与之前定义的save()方法一样,我们使用fetchById()来确保操作成功。

如何运行...

将步骤1至步骤5中描述的代码复制到Application/Database文件夹中的CustomerService.php文件中,并在其中定义一个chap_05_entity_to_query.php调用程序。让调用程序使用相应的类来初始化自动加载器。

<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
use Application\Database\CustomerService;

现在你可以创建一个服务的实例,并随机获取一个客户。然后,该服务将返回一个客户实体作为结果。

// 获取服务实例
$service = new CustomerService(new Connection(include __DIR__ . DB_CONFIG_FILE));

echo "\nSingle Result\n";
var_dump($service->fetchById(rand(1,79)));

这是输出。

现在将步骤6至15中的代码复制到服务类中。将要插入的数据添加到chap_05_entity_to_query.php调用程序中。然后我们使用这些数据生成一个Customer实体实例。

// 样本数据
$data = [
  'name'              => 'Doug Bierer',
  'balance'           => 326.33,
  'email'             => 'doug' . rand(0,999) . '@test.com',
  'password'          => 'password',
  'status'            => 1,
  'security_question' => 'Who\'s on first?',
  'confirm_code'      => 12345,
  'level'             => 'ADV'
];

// 建立新 Customer
$cust = Customer::arrayToEntity($data, new Customer());

然后我们可以检查调用save()前后的ID

echo "\nCustomer ID BEFORE Insert: {$cust->getId()}\n";
$cust = $service->save($cust);
echo "Customer ID AFTER Insert: {$cust->getId()}\n";

最后,我们修改余额,再次调用save(),查看结果。

echo "Customer Balance BEFORE Update: {$cust->getBalance()}\n";
$cust->setBalance(999.99);
$service->save($cust);
echo "Customer Balance AFTER Update: {$cust->getBalance()}\n";
var_dump($cust);

下面是调用程序的输出。

更多...

有关关系模型的更多信息,请参考https://en.wikipedia.org/wiki/Relational_model。关于RDBMS的更多信息,请参考https://en.wikipedia.org/wiki/Relational_database_management_system。关于PDOStatement::fetchObject()如何在构造函数之前插入属性值的信息,请看 "rasmus at mindplay dot dk "在php.net文档参考中关于fetchObject()的评论(http://php.net/manual/en/pdostatement.fetchobject.php)。