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

我有一个带有域对象和数据映射器的 Web MVC 应用程序.数据映射器的类方法包含所有数据库查询逻辑.我试图避免镜像任何数据库结构,因此,在构造 sql 语句时实现最大的灵活性.所以,原则上,我尽量不使用任何 ORM 或 ActiveRecord 结构/模式.

I have a web MVC application with domain objects and data mappers. The class methods of the data mappers contain all database querying logic. I'm trying to avoid mirroring any database structure and, therefore, to achieve the maximum flexibility in constructing the sql statements. So, in principle, I'm trying to not make use of any ORM or ActiveRecord structure/pattern AT ALL.

我举个例子:通常,我可以有一个抽象类 AbstractDataMapper 被所有特定的数据映射器类继承——比如 UserDataMapper 类.然后我可以在 AbstractDataMapper 中定义一个 findById() 方法,以获取特定表的记录 - 如 users - 通过给定的 id 值,例如用户身份.但这意味着我总是从单个表中获取记录,而无法使用任何左连接来从与给定 id - 用户 ID 对应的其他一些表中获取一些其他详细信息.

Let me give you an example: Normally, I could have an abstract class AbstractDataMapper inherited by all specific data mapper classes - like the UserDataMapper class. And then I could define a findById() method in AbstractDataMapper, to fetch a record of a specific table - like users - by a given id value, e.g. user id. But this would imply that I'd always fetch a record from a single table, without the possibility to use any left joins to also fetch some other details from some other tables corresponding to the given id - user id.

所以,我的问题是:在这些条件下 - 我自己必须要实现一个抽象数据映射器类,还是每个数据映射器类都应该包含它自己完全专有"的数据访问层实现?

So, my question is: Under these conditions - to which I myself obliged to, should I implement an abstract data mapper class, or each data mapper class should contain its own completely "proprietary" implementation of the data-access layer?

我希望我能清楚地表达我的想法.如果我不清楚或者您有任何问题,请告诉我.

I hope I could express my idea clear. Please tell me, if I was somehow unclear or you have any questions.

非常感谢您的时间和耐心.

Thank you very much for your time and patience.

推荐答案

如果我理解你的意思......

If I understood your point ...

让所有的具体映射器都从一个公共类继承 SQL 有几个你忽略的问题:

Having all your concrete mappers inheriting SQL from a common class has several issues that you have missed:

  • 域对象中的参数名称取决于列的名称
  • 有一个获取方法"在映射器中,没有相应的表
  • 您还有配置(表名),这是超类所期望的
  • 数据库架构必须将 id 作为所有 PRIMARY KEY 列的名称
  • parameter names in your domain objects depend on the names of columns
  • there is a "fetching method" in mappers, that don't have a corresponding table
  • you still have configuration (table name), that is expected by superclass
  • the DB schema must have id as name for all of your PRIMARY KEY columns

现在,我将尝试打开其中的每一个.

Now, I'm gonna try to unpack each of those.

要创建共享的 findById() 方法,唯一实用的方法是围绕以下内容构建它:

To create a shared findById() method, the only pragmatic approach is to build it around something like this:

"SELECT * FROM {$this->tableName} WHERE id = :id"

主要问题实际上是通配符 * 符号.

The main issue actually is the wildcard * symbol.

使用数据映射器填充实体有两种主要方法:使用设置器或使用反射.在这两种情况下,名称"都是参数/设置器的数量由您选择的列隐含.

There are two major approaches for populating an entity using a data mapper: use setters or use reflection. In both cases the "names" of a parameters/setters is implied by columns, that you have selected.

在普通查询中,您可以执行诸如SELECT name AS fullName FROM ... 之类的操作,这使您可以使用查询来重新命名 字段.但是对于统一方法",没有好的选择.

In a normal query you can do something like SELECT name AS fullName FROM ..., which lets you to use the query for re-naming the fields. But with a "unified approach", there are no good options.

所以,事情是,除非你有一个每个表的映射器结构(在这种情况下,活动记录开始看起来像务实的选项),你最终会得到很少(非常常见的)边缘情况".映射器的场景:

So, the thing is, unless you have a mapper-per-table structure (in which case an active record starts look like pragmatic option), you will end up with few (really common) "edge case" scenarios for your mappers:

  • 仅用于保存数据
  • 处理集合而不是单一实体
  • 聚合来自多个表的数据
  • 使用具有复合键的表
  • 它实际上不是一个表,而是一个 SQL 视图
  • ... 或以上的组合

您最初的想法在小规模项目中效果很好(一两个映射器是边缘情况").但是对于大型项目,findById() 的使用将是例外而不是常态.

Your original idea would work just fine in a small scale project (with one or two mappers being an "edge case"). But with a large project, the usage of findById() will be the exception not the norm.

要在超类中实际获得这个 findById() 方法,您需要一种将表名传达给它的方法.这意味着,您的类定义中有类似 protected $tableName 的内容.

To actually get this findById() method in the superclass, you will need a way to communicate the table name to it. Which would mean, that you have something like protected $tableName in you class definition.

您可以通过在抽象映射器类中使用 abstract function getTableName() 来缓解它,该类在实现时会返回一个全局常量值.

You can mitigate it by having abstract function getTableName() in your abstract mapper class, which, when implemented, returns a value of global constant.

但是,当您的映射器需要处理多个表时会发生什么.

But what happens, when your mapper need to work with multiple tables.

对我来说这似乎是一种代码味道,因为信息实际上跨越了两个边界(因为没有更好的词).当此代码中断时,超类中的 SQL 将显示错误,这不是错误的来源(特别是如果您使用常量).

It seems like a code smell to me, because information actually crosses two boundaries (for lack of better word). When this code breaks, the error will be shown for SQL in the superclass, which isn't where the error originated from (especially, if you go with constants).

这个观点有点争议:)

据我所知,调用所有主列 id 的做法来自各种 ORM.这招致的惩罚仅适用于可读性(和代码维护).考虑这两个查询:

As far as I can tell, the practice of calling all primary columns id comes from various ORMs. The penalty, that this incurs, applies only to readability (and code maintenance). Consider these two queries:

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"是什么变得越来越难.代表什么情况.

As the DB schema grows and the queries become more complex, it gets harder and harder to actually keep track of, what the "id" stands for in what case.

我的建议是尝试为列使用相同的名称,当它是主键和外键时(如果可能,因为在某些情况下,例如对于闭包表,它是不可行的").基本上,所有存储相同类型 ID 的列都应该具有相同的名称.

My recommendation would be to try same name for column, when it is a primary as when it is a foreign key (when possible, because in some cases, like for "closure tables, it's not viable). Basically, all columns that store IDs of same type, should have the same name.

作为小奖励,您可以获得 USING() 语法糖.

As a minor bonus, you get the USING() syntax sugar.

坏主意.你基本上打破了LSP.

Bad idea. You are basically breaking LSP.

相关文章