本文目录导读:

这是一个非常经典且核心的 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,使用原生的表网关思想来优化性能。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播