DataMapper模式要求实体完全 unaware of persistence,Mapper负责映射且不继承PDO;需避免N+1、循环依赖,SQL应写死并用PDO预处理,核心是解耦而非ORM功能。

PHP里手写DataMapper要绕开ActiveRecord思维
直接用find()或save()挂在实体类上,不是DataMapper,是ActiveRecord变体。DataMapper的核心约束是:实体(Domain Object)必须完全 unaware of persistence —— 它不该有save()、delete(),也不该知道数据库字段名或SQL。
实操时容易踩的坑:
• 把User类加上saveToDb()方法,就破戒了
• Mapper类里直接new一个User并赋值字段,而不是通过构造参数或setter注入数据
• 在Mapper里拼SQL字符串,而不是用PDO预处理或抽象查询构建器
正确做法示例:
// 实体干净无逻辑
class User
{
public function __construct(
public int $id,
public string $email,
public string $name
) {}
}
// Mapper独立承担映射职责
class UserMapper
{
public function __construct(private PDO $pdo) {}
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare('SELECT id, email, name FROM users WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? new User((int)$row['id'], $row['email'], $row['name']) : null;
}
public function insert(User $user): void
{
$stmt = $this->pdo->prepare('INSERT INTO users (email, name) VALUES (?, ?)');
$stmt->execute([$user->email, $user->name]);
$user->id = (int)$this->pdo->lastInsertId(); // 注意:ID回填是Mapper职责,非User自身行为
}
}
为什么不能让Mapper继承PDO或DB类
常见错误是写一个class UserMapper extends Database,以为能复用连接和事务方法。问题在于:Mapper不是数据库代理,它是「领域对象 ↔ 数据存储」之间的翻译官。继承PDO会模糊职责边界,导致测试困难、耦合加重、事务控制失控。
应该用组合而非继承:
- Mapper只持有一个
PDO实例或PDOStatement工厂,不暴露连接细节 - 事务应由上层协调(如Application Service),用
$pdo->beginTransaction()包裹多个Mapper调用,而不是在UserMapper::insert()里自己开事务 - 若需复用查询逻辑,提取为
QueryBuilder或SqlExecutor独立组件,而非让Mapper变重
关联关系映射最容易崩在N+1和循环依赖上
比如User有多个Post,手写Mapper时若在UserMapper::findById()里直接调用PostMapper::findByUserId(),就会触发N+1查询;更糟的是,如果PostMapper又反向依赖UserMapper去加载作者,就构成循环依赖。
解决路径很实际:
- 避免在Mapper方法内部调用其他Mapper —— 把关联加载拆成显式步骤,由调用方决定是否查、何时查
- 需要连表查时,写专用方法如
UserMapper::findByIdWithPosts(int $id),用JOIN一次性取回,不复用单表逻辑 - 对一对多,返回数组或
Collection对象,但Collection本身不带持久化能力(即不能$posts->save()) - 绝不让
User实体持有Post[]数组引用后,再让Post反向持有User引用 —— 这会导致序列化/深拷贝出错
ORM底层没那么玄,PDO+反射+约定就能跑起来
所谓“ORM底层”,抛开框架包装,核心就三件事:把对象属性映射到字段名(靠ReflectionClass读public属性或getter)、把SQL结果集转成对象(new $class($data)或hydrate())、把变更同步到底层(INSERT/UPDATE语句生成)。Doctrine或Eloquent的复杂性,80%来自缓存、事件、代理对象、DQL解析这些增强功能,不是DataMapper本质。
所以如果你只是想稳住业务模型与数据库解耦,重点该放在:
- 定义清晰的实体接口(哪怕空的
interface Entity) - Mapper类名严格匹配实体(
User→UserMapper),方法名直白(findById,findAllByStatus) - 所有SQL语句写死在Mapper内(不拼接字段名,不动态表名),牺牲一点灵活性换可读性和可测性
- 用
assert($user instanceof User)守住类型边界,别让Mapper返回数组或stdClass
真正难的从来不是怎么写Mapper,而是当业务开始要求软删除、多态关联、JSON字段自动序列化时,你得判断:这些是该塞进Mapper,还是交给专门的Repository或Specification层 —— 而这个边界,文档不会告诉你,只有改三次代码后才清楚。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











