问题 使用数据映射器模式,实体(域对象)是否应该了解Mapper?


我是第一次使用Doctrine2,但我认为这个问题非常通用,不依赖于特定的ORM。

数据映射器模式中的实体是否应该知道 - 和 使用  - Mapper

我有几个具体的例子,但它们似乎都归结为同样的一般性问题。

如果我正在处理来自外部源的数据 - 例如a User 有很多 Messages  - 外部源只提供最新的几个实体(如RSS提要),怎么可以 $user->addMessage($message) 检查重复项,除非它知道Mapper,或者“搜索”整个集合(看起来像一个低效的事情)。

当然,控制器或事务脚本可以在将消息添加到用户之前检查重复项 - 但这似乎不太正确,并且会导致代码重复。

如果我有一个大集合 - 再来一个 User 与许多 Messages  - 怎么可能 User 实体为集合提供限制和分页而不实际代理Mapper调用?

同样,Controller或Transaction Script或使用Entity的任何东西都可以直接使用Mapper来检索它的集合 UserMessages 受到计数,日期范围或其他因素的限制 - 但这也会导致代码重复。

答案是使用存储库并使实体了解它们? (至少对于Doctrine2,以及其他ORM使用的类似概念。)此时,实体仍然与Mapper相对分离。


2444
2017-09-17 20:13


起源



答案:


规则#1:保持您的域模型简单明了。

首先,不要过早地优化某些东西,因为你认为它可能效率低下。构建域以使对象和语法正确流动。保持接口干净:$ user-> addMessage($ message)干净,精确且明确。在引擎盖下,您可以使用任意数量的模式/技术来确保维护完整性(缓存,查找等)。您可以利用服务来协调(复杂)对象依赖,这可能是过度的,但这是一个基本的示例/想法。

class User
{
  public function addMessage(Message $message)
  {
     // One solution, loop through all messages first, throw error if already exists
     $this->messages[] $message;
  }
  public function getMessage()
  {
     return $this->messages;
  }
}
class MessageService
{
  public function addUserMessage(User $user, Message $message)
  {
     // Ensure unique message for user
     // One solution is loop through $user->getMessages() here and make sure unique
     // This is more or less the only path to adding a message, so ensure its integrity here before proceeding 
     // There could also be ACL checks placed here as well
     // You could also create functions that provide checks to determine whether certain criteria are met/unmet before proceeding
     if ($this->doesUserHaveMessage($user,$message)) {
       throw Exception...
     }
     $user->addMessage($message);
  }
  // Note, this may not be the correct place for this function to "live"
  public function doesUserHaveMessage(User $user, Message $message)
  {
     // Do a database lookup here
     return ($user->hasMessage($message) ? true
  }
}
class MessageRepository
{
  public function find(/* criteria */)
  {
     // Use caching here
     return $message;
  }
}

class MessageFactory
{
   public function createMessage($data)
   {
     //
     $message = new Message();
     // setters
     return $message;
   }
}

// Application code
$user = $userRepository->find(/* lookup criteria */);
$message = $messageFactory->create(/* data */);
// Could wrap in try/catch
$messageService->sendUserMessage($user,$message);

一直在使用Doctrine2。您的域实体对象就是那些对象......他们不应该知道它们来自何处,域模型只管理它们并将它们传递给管理和操作它们的各种函数。

回顾过去,我不确定我是否完全回答了你的问题。但是,我不认为实体本身应该可以访问映射器。创建服务/存储库/无论在对象上操作什么,并在这些功能中使用适当的技术......

从发病开始也不要过度工程化。当性能实际上是一个问题时,让您的域名专注于其目标和重构。


8
2017-09-23 22:04



是的,我和你在一起设计模式,循环遍历所有消息是一个简单的解决方案 - 但这意味着(假设大量的消息)你正在加载存储中的所有记录,当一个简单的查询会给出相同的结果。 - Tim Lytle
是的,这就是为什么我提到可能使用doUserHaveMessage函数(或类似的东西)。这可能是查询完成的地方。它仍然保持您的实体清洁和专注,但允许您保持您的域一致。此外,您可以从一个简单/简单的实现(循环数组)开始,但稍后在性能需要它时重构...只要您的接口保持不变,其他代码就不必更改。 - jsuggs


答案:


规则#1:保持您的域模型简单明了。

首先,不要过早地优化某些东西,因为你认为它可能效率低下。构建域以使对象和语法正确流动。保持接口干净:$ user-> addMessage($ message)干净,精确且明确。在引擎盖下,您可以使用任意数量的模式/技术来确保维护完整性(缓存,查找等)。您可以利用服务来协调(复杂)对象依赖,这可能是过度的,但这是一个基本的示例/想法。

class User
{
  public function addMessage(Message $message)
  {
     // One solution, loop through all messages first, throw error if already exists
     $this->messages[] $message;
  }
  public function getMessage()
  {
     return $this->messages;
  }
}
class MessageService
{
  public function addUserMessage(User $user, Message $message)
  {
     // Ensure unique message for user
     // One solution is loop through $user->getMessages() here and make sure unique
     // This is more or less the only path to adding a message, so ensure its integrity here before proceeding 
     // There could also be ACL checks placed here as well
     // You could also create functions that provide checks to determine whether certain criteria are met/unmet before proceeding
     if ($this->doesUserHaveMessage($user,$message)) {
       throw Exception...
     }
     $user->addMessage($message);
  }
  // Note, this may not be the correct place for this function to "live"
  public function doesUserHaveMessage(User $user, Message $message)
  {
     // Do a database lookup here
     return ($user->hasMessage($message) ? true
  }
}
class MessageRepository
{
  public function find(/* criteria */)
  {
     // Use caching here
     return $message;
  }
}

class MessageFactory
{
   public function createMessage($data)
   {
     //
     $message = new Message();
     // setters
     return $message;
   }
}

// Application code
$user = $userRepository->find(/* lookup criteria */);
$message = $messageFactory->create(/* data */);
// Could wrap in try/catch
$messageService->sendUserMessage($user,$message);

一直在使用Doctrine2。您的域实体对象就是那些对象......他们不应该知道它们来自何处,域模型只管理它们并将它们传递给管理和操作它们的各种函数。

回顾过去,我不确定我是否完全回答了你的问题。但是,我不认为实体本身应该可以访问映射器。创建服务/存储库/无论在对象上操作什么,并在这些功能中使用适当的技术......

从发病开始也不要过度工程化。当性能实际上是一个问题时,让您的域名专注于其目标和重构。


8
2017-09-23 22:04



是的,我和你在一起设计模式,循环遍历所有消息是一个简单的解决方案 - 但这意味着(假设大量的消息)你正在加载存储中的所有记录,当一个简单的查询会给出相同的结果。 - Tim Lytle
是的,这就是为什么我提到可能使用doUserHaveMessage函数(或类似的东西)。这可能是查询完成的地方。它仍然保持您的实体清洁和专注,但允许您保持您的域一致。此外,您可以从一个简单/简单的实现(循环数组)开始,但稍后在性能需要它时重构...只要您的接口保持不变,其他代码就不必更改。 - jsuggs


IMO,一个实体应该忘记它来自何处,谁创建它以及如何填充其相关实体。在ORM中我使用(我自己的)我能够定义两个表之间的连接并通过指定(在C#中)来限制其结果:

SearchCriteria sc = new SearchCriteria();
sc.AddSort("Message.CREATED_DATE","DESC");
sc.MaxRows = 10;
results = Mapper.Read(sc, new User(new Message());

这将导致一个限制为10个项目的连接,按日期创建消息排序。消息项目将添加到每个用户。如果我写:

results = Mapper.Read(sc, new  Message(new User());

联接是相反的。

因此,可以使实体完全不知道映射器。


1
2017-09-17 20:32



是的,我倾向于希望实体与任何东西脱钩。你的方法大致就是我的意思,“...控制器或事务脚本或任何正在使用实体的东西都可以直接使用Mapper来检索受限于......的用户消息的集合。”检查是否有问题怎么样? Message 已经存在?只是在实体之外做吗? - Tim Lytle
我假设你正在消费来自第三方服务的消息(如你所提到的RSS提要)。在这种情况下,您可以搜索收到的最后一条消息;如果没有找到你添加它。继续对其他消息执行此操作,直到找到存在的消息 - 这意味着您已经赶上了它们(除非我不了解您的要求) - Otávio Décio
ávio那会起作用,但它会在实体之外发生 - 对吗?虽然我想让我的实体分离,但它似乎也像添加一个代码一样 Message 应该在实体中(以避免代码重复)。 - Tim Lytle
@Tim - 我绝对是 没有 我的实体中与业务相关的代码。在我的模型中添加新消息需要创建一个新的Message对象,将其UserId外键分配给我正在使用的用户,然后分配其他信息,然后调用Mapper.Write(Message)。再次,完全在实体之外。 - Otávio Décio


没有。

这就是原因:信任。您不能相信数据会对系统带来好处。您只能信任系统对数据采取行动。这是编程逻辑的基础。

让我们说一些讨厌的东西插入到数据中,它是用于XSS的。如果数据块正在执行操作或者如果它被评估,那么XSS代码将被混合到事物中并且它将打开安全漏洞。

左手不要知道右手做什么! (主要是因为你不想知道)


1
2017-09-20 18:57



那么处理我描述的一般情况的最佳方法是什么?只需将其保存在控制器/事务脚本/中 使用 实体? - Tim Lytle
将其推送到新的控件类中。考虑编码就像编写一堆工具一样,总是更好。你永远不会把锤子系在某人的手臂上 - 你给他们机会挑选一把并使用它。您的数据也是如此。您必须收紧代码,以便它可以根据数据执行某些有效操作,但必须遵守规则。当您让数据控制系统时,您会给数据一个损坏系统的机会。 - Geekster
我想我认为实体(或模型)是代表和验证数据的东西。 - Tim Lytle
数据模型无法自我验证。这违反了哲学法。看看Putnam的大脑在Vat思想实验: en.wikipedia.org/wiki/Brain_in_a_vat  这意味着数据将假设它是完美的,但如果它在道德上受到损害,它将不会知道它已被破坏(按设计)。没有系统可以自我评估。这就像是建议学生可以评分他自己的论文。只有存在于数据范围之外的系统才能评估数据。希望这对你来说足够了,但我愿意继续辩论,如果它有所帮助。此外,数据必须是可移植的。 - Geekster
我想到了更多,我还有别的东西要补充。每当有数据正在调节系统的功能时,例如设置文件或其他东西,您可以保留默认值以防出现问题但是如果它们有效则将其处理为经过验证。如果用户可以更改它们,那么请小心处理它们。它与数据不同。你没有以同样的方式处理它。它的选项,仔细权衡,他们会修改系统。你仍然没有评估他们。你永远不会评估一个设置文件。但是从用户加载系统的完整部分是危险的。别!:) - Geekster