PHP项目数据映射器与表网关

wen PHP项目 2

本文目录导读:

PHP项目数据映射器与表网关

  1. 核心定义
  2. 核心区别对比表
  3. PHP 代码示例对比
  4. 经典 PHP 框架中的体现
  5. 如何选择?(决策树)

这是一个非常经典且核心的 PHP 架构对比问题。数据映射器(Data Mapper)表网关(Table Data Gateway)都是用于处理数据库持久化的模式,但它们的职责范围和抽象层级完全不同。

简而言之:表网关操作的是(SQL),数据映射器操作的是对象(内存)。

以下从定义、核心区别、代码示例、优缺点以及适用场景几个方面为你详细拆解。


核心定义

  • 表数据网关

    • 视角:数据库视角。
    • 职责:一个类对应数据库中的一张表,该类封装了对这张表的所有 SQL 操作(CRUD)。
    • 工作方式:它接收原始数据(如数组或简单的 stdClass),执行 SQL,然后返回原始数据。
    • 关键点:它不关心领域对象,只关心表的行数据。
  • 数据映射器

    • 视角:领域模型(对象)视角。
    • 职责:一个类负责在领域对象数据库之间进行双向数据转换,它知道如何将对象的状态保存到数据库,也如何从数据库加载数据恢复对象。
    • 工作方式:它接收领域对象,将对象属性拆解为 SQL 值,执行 SQL,再将结果集组装回完整的领域对象。
    • 关键点:领域对象对数据库完全无感知(持久化无知)。

核心区别对比表

特性 表数据网关 数据映射器
核心关注点 SQL 操作 对象-关系映射
操作单元 表的行(数组/简单对象) 领域模型对象(有业务逻辑)
对象持久化无知 ❌ 依赖数据库结构 ✅ 完全不依赖(纯 PHP 对象)
业务逻辑位置 通常在 Service 层(外部分离) 通常内聚在领域对象内(充血模型)
典型返回值 arraystdClass 领域模型对象 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 对象,通过 EntityManagerRepository 来持久化,对 PDO 或 SQL 完全无感。
  • 传统 CodeIgniter / Yii1:倾向于表数据网关模式(如 $this->db->get('users') 返回数组),简单直接。

如何选择?(决策树)

选择 表数据网关 当:

  1. 项目规模小或简单:CRUD 为主,业务逻辑不复杂。
  2. 性能敏感且需要手动优化 SQL:比如统计报表、大量数据批量操作。
  3. 团队对 ORM 不熟悉:学习成本低,SQL 可见。
  4. 项目作为数据提供层:需要将数据以 JSON/数组形式提供,不需要复杂对象。

选择 数据映射器 (或完整 ORM) 当:

  1. 项目有复杂业务逻辑:领域模型内有大量规则,需要聚合根、值对象、领域事件。
  2. 需要处理复杂的对象关联:多对多、继承映射、嵌套对象。
  3. 希望保持领域模型纯净:依赖反转,便于单元测试(可 Mock Mapper)。
  4. 项目是大型、长期演进的企业级应用:Doctrine 等成熟映射器提供了缓存、延迟加载、变更追踪等高级功能。
  • 表网关:面向数据表编程,你是数据库管理员,直接操作行数据。
  • 数据映射器:面向对象编程,你是模型设计师,关心对象的状态和行为,数据库只是存储机制。

实际项目中,两者并非完全对立,很多项目会在 ORM(数据映射器)之上,针对复杂查询建立一层查询服务(Query Service)自定义 Repository,使用原生的表网关思想来优化性能。

抱歉,评论功能暂时关闭!