本文目录导读:

这是一个非常经典且核心的 PHP 架构对比问题。数据映射器(Data Mapper)和表网关(Table Data Gateway)都是用于处理数据库持久化的模式,但它们的职责范围和抽象层级完全不同。
简而言之:表网关操作的是表(SQL),数据映射器操作的是对象(内存)。
以下从定义、核心区别、代码示例、优缺点以及适用场景几个方面为你详细拆解。
核心定义
-
表数据网关
- 视角:数据库视角。
- 职责:一个类对应数据库中的一张表,该类封装了对这张表的所有 SQL 操作(CRUD)。
- 工作方式:它接收原始数据(如数组或简单的
stdClass),执行 SQL,然后返回原始数据。 - 关键点:它不关心领域对象,只关心表的行数据。
-
数据映射器
- 视角:领域模型(对象)视角。
- 职责:一个类负责在领域对象和数据库之间进行双向数据转换,它知道如何将对象的状态保存到数据库,也如何从数据库加载数据恢复对象。
- 工作方式:它接收领域对象,将对象属性拆解为 SQL 值,执行 SQL,再将结果集组装回完整的领域对象。
- 关键点:领域对象对数据库完全无感知(持久化无知)。
核心区别对比表
| 特性 | 表数据网关 | 数据映射器 |
|---|---|---|
| 核心关注点 | SQL 操作 | 对象-关系映射 |
| 操作单元 | 表的行(数组/简单对象) | 领域模型对象(有业务逻辑) |
| 对象持久化无知 | ❌ 依赖数据库结构 | ✅ 完全不依赖(纯 PHP 对象) |
| 业务逻辑位置 | 通常在 Service 层(外部分离) | 通常内聚在领域对象内(充血模型) |
| 典型返回值 | array,stdClass |
领域模型对象 User |
| 关联关系处理 | 手动写 JOIN 或额外查询 | 自动处理关联、懒加载、级联 |
| 复杂性 | 低 | 高(需要维护映射元数据) |
| 测试难度 | 易(可 Mock 数据库) | 较复杂(需要处理对象图) |
| 性能 | 可精确控制 SQL,性能较好 | 可能产生 N+1 问题(ORM 需优化) |
PHP 代码示例对比
假设我们有一个 User 表(id, name, email, created_at)和一个领域对象 User。
1 表数据网关示例
<?php
// 1. 定义表网关类 (只关心 SQL)
class UserTableGateway {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function find(int $id): ?array { // 返回数组
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function insert(array $data): int { // 接收数组
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$data['name'], $data['email']]);
return (int)$this->pdo->lastInsertId();
}
public function update(int $id, array $data): void {
$stmt = $this->pdo->prepare('UPDATE users SET name = ?, email = ? WHERE id = ?');
$stmt->execute([$data['name'], $data['email'], $id]);
}
}
// 2. 在 Service 层使用
class UserService {
private UserTableGateway $gateway;
public function register(string $name, string $email): void {
// 手动进行数据校验和业务逻辑
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
// 将原始数据传递给网关
$this->gateway->insert(['name' => $name, 'email' => $email]);
}
}
2 数据映射器示例
<?php
// 1. 定义领域对象 (纯对象,无数据库依赖)
class User {
public function __construct(
private ?int $id,
private string $name,
private string $email,
private ?DateTimeImmutable $createdAt = null
) {}
// 业务方法 (充血模型)
public function changeEmail(string $newEmail): void {
// 业务规则内聚在对象内部
if ($newEmail === $this->email) return;
// 进行域名校验等
$this->email = $newEmail;
}
// Getters...
public function getId(): ?int { return $this->id; }
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
}
// 2. 定义数据映射器 (负责对象 <-> 数据库的转换)
class UserMapper {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// 方法返回 User 对象,而不是数组
public function find(int $id): ?User {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) return null;
// 手动将数据行映射回对象
return new User(
id: (int)$row['id'],
name: $row['name'],
email: $row['email'],
createdAt: new DateTimeImmutable($row['created_at'])
);
}
// 保存对象的状态到数据库
public function save(User $user): void {
if ($user->getId() === null) {
// Insert
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$user->getName(), $user->getEmail()]);
// 使用反射或 setter 设置新 ID(常用做法)
$user->setId((int)$this->pdo->lastInsertId());
} else {
// Update
$stmt = $this->pdo->prepare('UPDATE users SET name = ?, email = ? WHERE id = ?');
$stmt->execute([$user->getName(), $user->getEmail(), $user->getId()]);
}
}
}
// 3. 在业务层使用
$user = new User(null, 'John', 'john@example.com');
$user->changeEmail('new@example.com'); // 业务逻辑在对象内部
$mapper = new UserMapper($pdo);
$mapper->save($user); // 对象自动持久化
经典 PHP 框架中的体现
- Laravel (Eloquent ORM):本质上是 Active Record 模式(数据映射器 + 表网关的混合体)。
- 一个
Model类既包含 SQL 查询方法(表网关的功能),又代表单行数据,还包含业务逻辑。
- 一个
- Doctrine 2:是纯正的数据映射器实现。
- 你的实体类(
User)是纯 PHP 对象,通过EntityManager和Repository来持久化,对 PDO 或 SQL 完全无感。
- 你的实体类(
- 传统 CodeIgniter / Yii1:倾向于表数据网关模式(如
$this->db->get('users')返回数组),简单直接。
如何选择?(决策树)
选择 表数据网关 当:
- 项目规模小或简单:CRUD 为主,业务逻辑不复杂。
- 性能敏感且需要手动优化 SQL:比如统计报表、大量数据批量操作。
- 团队对 ORM 不熟悉:学习成本低,SQL 可见。
- 项目作为数据提供层:需要将数据以 JSON/数组形式提供,不需要复杂对象。
选择 数据映射器 (或完整 ORM) 当:
- 项目有复杂业务逻辑:领域模型内有大量规则,需要聚合根、值对象、领域事件。
- 需要处理复杂的对象关联:多对多、继承映射、嵌套对象。
- 希望保持领域模型纯净:依赖反转,便于单元测试(可 Mock Mapper)。
- 项目是大型、长期演进的企业级应用:Doctrine 等成熟映射器提供了缓存、延迟加载、变更追踪等高级功能。
- 表网关:面向数据表编程,你是数据库管理员,直接操作行数据。
- 数据映射器:面向对象编程,你是模型设计师,关心对象的状态和行为,数据库只是存储机制。
实际项目中,两者并非完全对立,很多项目会在 ORM(数据映射器)之上,针对复杂查询建立一层查询服务(Query Service)或自定义 Repository,使用原生的表网关思想来优化性能。