PHP MVC: 数据映射器模式:类设计

3
我有一个Web MVC应用程序,其中包含域对象和数据映射器。数据映射器的类方法包含了所有数据库查询逻辑。我试图避免镜像任何数据库结构,并因此在构建SQL语句时实现最大的灵活性。因此,原则上,我试图根本不使用任何ORM或ActiveRecord结构/模式。
让我举个例子: 通常,我可以有一个抽象类AbstractDataMapper,被所有特定的数据映射器类继承 - 如UserDataMapper类。然后我可以在AbstractDataMapper中定义findById()方法,通过给定的id值(例如用户ID)来获取特定表 - 如users - 的记录。但这将意味着我总是从单个表中获取记录,无法使用任何左连接来获取与给定id - 用户ID相对应的其他表中的一些其他详细信息。
所以,我的问题是: 在这些条件下 - 我自己强制执行的条件,我应该实现一个抽象的数据映射器类,还是每个数据映射器类应该包含其自己完全“专有”的数据访问层实现?
我希望我能清楚地表达我的想法。如果我有所不清楚或您有任何问题,请告诉我。
非常感谢您的时间和耐心。

1
作为一个曾经自掏五百点赏金的人,我要给你一个免费的忠告:千万别这么做。后来我不得不请求管理员删除了那篇帖子,因为我得到的回答都是垃圾。这会吓跑那些不太自信的人。如果问题不是那么紧急,你可以在两周内提供一百点赏金,效果会更好。用五百点的预算,在“悬赏”版块中固定一篇帖子超过一个月也是很不错的选择。 - tereško
@tereško 感谢您的评论和好意建议。我之前不知道这一点,因此我重新编辑了赏金部分。最终,我的目标确实是吸引到好的答案。 - user7941334
1个回答

5
如果我理解你的观点的话...
让所有具体的映射器从一个共同的类继承SQL存在几个问题,你已经忽略了:
- 领域对象中的参数名称取决于列名 - 映射器中存在一个“获取方法”,它没有相应的表 - 你仍然有配置(表名),超类期望这样做 - 数据库架构必须将id作为所有PRIMARY KEY列的名称
现在,我要尝试解释一下每个问题。

参数和列名

为了创建一个共享的findById()方法,唯一实用的方法就是围绕着这样的东西构建它:
"SELECT * FROM {$this->tableName} WHERE id = :id"

主要问题实际上是通配符*符号。
使用数据映射器填充实体有两种主要方法:使用setter或使用反射。在这两种情况下,参数/设置器的“名称”都是由所选列隐含的。
在正常查询中,您可以执行类似于SELECT name AS fullName FROM ...的操作,这使您可以使用查询进行字段的重命名。但是,在“统一方法”中,没有很好的选择。
每个映射器都可以通过id获取数据吗?
因此,问题在于,除非您具有每个表的映射器结构(在这种情况下,活动记录开始看起来像是实用的选项),否则您将面临您的映射器的几个(真正常见的)“边缘情况”场景:
  • 仅用于保存数据
  • 处理集合而不是单个实体
  • 从多个表聚合数据
  • 使用具有组合键的表
  • 它实际上不是表,而是SQL视图
  • ... 或以上的组合
你的原始想法在小规模项目中可以很好地工作(其中一个或两个映射器是“边缘情况”)。但是在大型项目中,findById() 的使用将成为异常而不是常态。
独立的父母?
要实际在超类中获得这个findById() 方法,您需要一种将表名传递给它的方法。这意味着您在类定义中有像protected $tableName 这样的东西。
您可以通过在抽象映射器类中拥有abstract function getTableName() 来减轻这种情况,该函数在实现时返回全局常量值。
但是当您的映射器需要处理多个表时会发生什么。
对我来说,这似乎是一种代码异味,因为信息实际上跨越了两个边界(缺乏更好的词语)。当此代码出现错误时,错误将显示在超类中的 SQL 中,而不是错误源(特别是如果您使用常量)。
命名主键
这是一个更具争议性的观点 :)
据我所知,将所有主键列称为id的做法来自于各种ORM。这样做的代价只体现在可读性(和代码维护)上。考虑以下两个查询:
SELECT ar.id, ac.id 
  FROM Articles AS ar LEFT JOIN 
       Accounts AS ac ON ac.id = ar.account_id 
 WHERE ar.status = 'published'

SELECT ar.article_id, ac.account_id 
  FROM Articles AS ar LEFT JOIN 
       Accounts AS ac USING(account_id)
 WHERE ar.status = 'published'

随着数据库模式的增长和查询变得越来越复杂,实际上很难跟踪"id"在什么情况下代表什么。我的建议是,当列是主键时,尽可能使用相同的名称作为外键(如果可能的话,因为在某些情况下,例如对于“闭包表”,这是不可行的)。基本上,所有存储相同类型ID的列都应该有相同的名称。作为一个小奖励,您可以获得USING()语法糖。
TL;DR: 不好的想法。您基本上正在违反LSP

我刚刚看了你的回答,发现我需要更多时间——现在有点累。我只想说:非常感谢你,tereško。我一定会稍后深入阅读它。 - user7941334
嗨。我已经仔细阅读了您的答案,多次。哇 :-) 如果可以的话,我想就您答案中的两个问题向您请教一下。您的所有论点都是围绕findById()示例构建的。这意味着您只是想展示一个案例,但也适用于其他映射器方法/情况。我理解得对吗?如果是这样,那么是的,您非常好地理解了我的观点。我担心只会得到关于使用findById()方法的答案,而不考虑整个数据映射器结构/设计。 - user7941334
我的第二个问题是关于第(3)部分的:“通过“代码异味……信息实际上跨越了两个边界”,您是指我可以一次从多个表中获取数据,还是指类结构方面的某些内容?”。 - user7941334
我的最后一个问题,为了确保:在TL;DR中,你的意思是“实现抽象数据映射器类是个坏主意,因为你基本上是通过使用它来破坏LSP”吗?很抱歉打扰你这样的问题,但这些问题是我无法将自己的理解与你的观点相一致的问题。再次感谢你的出色回答。 - user7941334
findById() 只是任何 SQL 代码的方便示例,已移动到超类,甚至像 delete()exists() 这样的方法(它们不会受到“通配符问题”的影响)。至于“两个边界”部分... 好吧... 很难确定它甚至看起来像一个问题。归根结底是调试。如果数据库模式发生变化,有人不知道所有部分,这种变化将影响 PHP 代码(在实际拥有团队的大型项目中常见问题)。 - tereško
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接