本文目录导读:

我们来详细解析一下 PHP 项目中的 行数据网关 和 记录集 这两个概念,它们都是数据源架构模式中的重要组成部分,尤其是在传统的 PHP 项目中(如使用 MySQLi 或 PDO 扩展,或者基于 ActiveRecord 模式之前的框架中)非常常见。
核心概念理解
行数据网关
- 定义:一个对象,它充当一个单独的行记录与数据库之间的“网关”或“接口”。
- 核心思想:将数据库查询、插入、更新、删除操作(CRUD)从业务逻辑中分离出来,封装到一个专门的对象中,这个对象的每个实例通常对应数据库表中的一行数据。
- 特点:
- 领域模型简单:通常没有复杂的业务逻辑,主要是数据的 getter/setter 和简单的数据校验。
- 强耦合于表结构:一个行数据网关类通常对应一个数据库表。
- 无状态:对象本身不维护连接,每次操作都需要传入数据库连接。
记录集
- 定义:它是一种数据结构,用来存储从数据库查询返回的一组或多行数据的集合。
- 常见实现:
- 在 PHP 的 mysqli 扩展中,是
mysqli_result对象。 - 在 PDO 中,是
PDOStatement对象(执行查询后,通过fetchAll()等方法可以获取数据)。
- 在 PHP 的 mysqli 扩展中,是
- 特点:
- 包含元数据:除了数据行,还包含字段名、字段类型等描述信息。
- 可遍历:通常可以通过循环(如
while($row = ...)) 逐行获取数据。 - 不可变性/一次性使用:在某些实现中,记录集在获取完所有数据或关闭后,无法再次使用(除非重新查询)。
- 数据访问方式:通常通过游标(cursor)进行访问,可以顺序读取或随机读取(取决于实现)。
两者之间的关系与区别
这是理解这两个概念的关键点。
| 特性 | 行数据网关 | 记录集 |
|---|---|---|
| 粒度 | 一行数据(一个对象实例代表一行)。 | 多行数据(一个对象/结构代表整个结果集)。 |
| 封装层次 | 更上层,封装了 SQL 操作(增删改查)和字段映射。 | 更底层,是数据库查询结果的直接、原始的包装。 |
| 角色 | 主动的,可以执行 save()、update()、delete() 等方法。 |
被动的,主要作为数据容器,通常不提供直接的修改数据库方法。 |
| 面向对象 | 高,是真正的对象,每个字段有 getter/setter。 | 低,更像是一个数组的集合或迭代器。 |
| 典型应用 | 与 ActiveRecord 模式类似,但更关注于简单的数据映射和操作。 | 常用于简单查询,或者作为 ORM(对象关系映射)的底层数据来源。 |
一句话总结: 记录集是数据从数据库拿到内存后的原始样子(一堆数组)。行数据网关是把记录集中的一行(一个数组)包装成一个对象,这个对象知道如何保存自己、更新自己或删除自己。
PHP 代码示例
我们来通过一个简单的 users 表来演示。
记录集(使用 PDO)
记录集通常是直接使用 PDO 查询得到的结果。
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 查询所有用户,得到的就是一个记录集(PDOStatement 对象)
$stmt = $pdo->prepare('SELECT id, name, email, created_at FROM users WHERE active = :active');
$stmt->execute(['active' => 1]);
// 记录集 $stmt 包含了所有行数据
// 我们可以用 fetchAll() 将所有行放入一个数组(这是一个数组的记录集)
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
// $row 是一个关联数组:['id' => 1, 'name' => '张三', ...]
echo $row['name'] . ' - ' . $row['email'] . PHP_EOL;
}
// 也可以逐行获取,使用游标(cursor)
$stmt->execute(['active' => 1]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo $row['name'] . ' - ' . $row['email'] . PHP_EOL;
}
?>
- 特点:数据是数组形式,没有对象方法,无法直接保存回去。
行数据网关
我们将每一行数据封装成一个 User 对象,这个对象拥有访问数据库的能力。
<?php
// 行数据网关类
class UserGateway {
private $pdo;
// 属性对应表字段
public $id;
public $name;
public $email;
public $active;
public $created_at;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// 从数据库加载一行数据到当前对象
public function findById(int $id): ?self {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
// 将数组属性映射到对象属性
return $this->map($row);
}
// 将查询结果映射到对象
private function map(array $row): self {
$this->id = $row['id'];
$this->name = $row['name'];
$this->email = $row['email'];
$this->active = $row['active'];
$this->created_at = $row['created_at'];
return $this;
}
// 保存当前对象(插入或更新)
public function save(): bool {
if ($this->id) {
// 更新
$stmt = $this->pdo->prepare('UPDATE users SET name = :name, email = :email, active = :active WHERE id = :id');
return $stmt->execute([
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'active' => $this->active
]);
} else {
// 插入
$stmt = $this->pdo->prepare('INSERT INTO users (name, email, active, created_at) VALUES (:name, :email, :active, NOW())');
$result = $stmt->execute([
'name' => $this->name,
'email' => $this->email,
'active' => $this->active
]);
if ($result) {
$this->id = $this->pdo->lastInsertId();
}
return $result;
}
}
// 删除当前对象
public function delete(): bool {
if (!$this->id) return false;
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = :id');
return $stmt->execute(['id' => $this->id]);
}
// 静态方法:获取所有用户(返回对象数组)—— 这里通常需要配合集合对象或工厂
public static function findAllActive(PDO $pdo): array {
$stmt = $pdo->prepare('SELECT * FROM users WHERE active = :active');
$stmt->execute(['active' => 1]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$gateways = [];
foreach ($rows as $row) {
$gateway = new self($pdo);
$gateways[] = $gateway->map($row);
}
return $gateways;
}
}
// 使用行数据网关
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'password');
// 1. 创建一个新用户
$user = new UserGateway($pdo);
$user->name = '李四';
$user->email = 'lisi@example.com';
$user->active = 1;
$user->save();
echo "新用户ID: " . $user->id; // 输出 ID
// 2. 加载并更新一个用户
$user = new UserGateway($pdo);
$loadedUser = $user->findById(1);
if ($loadedUser) {
$loadedUser->name = '张三(已修改)';
$loadedUser->save();
}
// 3. 删除用户
$user = new UserGateway($pdo);
$loadedUser = $user->findById(2);
if ($loadedUser) {
$loadedUser->delete();
}
// 4. 获取所有活跃用户(对象数组)
$activeUsers = UserGateway::findAllActive($pdo);
foreach ($activeUsers as $user) {
echo $user->name . ' - ' . $user->email . PHP_EOL;
}
?>
- 特点:每个
UserGateway对象代表数据库中的一行,可以独立进行save()、delete()等操作,它封装了 SQL 语句,业务逻辑(如修改用户)只需要操作对象属性,然后调用save()即可。
行数据网关与记录集的优缺点
| 模式 | 优点 | 缺点 |
|---|---|---|
| 记录集 | 简单直接:代码量少,易于理解和编写。 性能较好:没有额外的对象创建开销(特别是批量查询时)。 灵活:可以轻松地使用原生 SQL 进行复杂查询。 |
散落 SQL:SQL 语句分散在业务逻辑中,难以维护和复用。 非面向对象:数据是数组/原始类型,缺少行为(方法),业务逻辑与数据分离。 代码冗余:同样的查询逻辑(如“按ID获取用户”)会在多个地方重复。 |
| 行数据网关 | 更好的封装性:SQL 语句被隐藏,业务逻辑更干净。 代码复用:CRUD 操作集中在网关类中,修改表结构时只需修改一处。 业务逻辑可扩展:可以在网关中添加校验规则、计算属性等。 测试更友好:可以轻松 mock 网关类。 |
对象开销:每行数据都要实例化一个对象,内存和性能开销略大。 实现复杂:需要额外编写映射代码。 查询灵活性降低:对于复杂查询(如 JOIN),需要在网关中编写专门的查询方法,或返回记录集。 |
总结与选择建议
-
何时使用记录集?
- 简单脚本/小项目:例如一个简单的博客、API 端点,不需要复杂的业务逻辑。
- 批量数据导出/报表:只需要读取数据,不需要修改。
- 性能敏感的场景:需要快速处理大量只读数据,避免对象创建开销。
-
何时使用行数据网关?
- 中型到大型项目:业务逻辑复杂,需要清晰的代码结构和可维护性。
- 数据需要修改/保存:需要对单行数据进行频繁的创建、更新、删除操作。
- 作为 ORM 的轻量级替代:当不想引入复杂的 ORM 框架(如 Doctrine、Eloquent)时,行数据网关是一个很好的中层解决方案。
-
现实中的组合使用:
- 一个 PHP 项目中往往同时使用两者。
UserGateway::findAllActive()方法内部会使用 PDO 获取记录集,然后遍历记录集,将每一行数据实例化为一个UserGateway对象,这样,你既获得了记录集查询的灵活性,又得到了对象的好处。
// 组合使用:记录集用于查询,行数据网关用于操作 $stmt = $pdo->query('SELECT * FROM users'); $rows = $stmt->fetchAll(); // 这是记录集 $users = []; foreach ($rows as $row) { $user = new UserGateway($pdo); // 创建行数据网关对象 $user->map($row); // 填充数据 $users[] = $user; }
希望这个详细的解释能帮助你清晰地理解 PHP 中的行数据网关与记录集。