【问题标题】:Using the Data Mapper Pattern, Should the Entities (Domain Objects) know about the Mapper?使用数据映射器模式,实体(域对象)是否应该知道映射器?
【发布时间】:2011-04-13 21:55:12
【问题描述】:

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

Data Mapper 模式中的实体是否应该了解并使用 - Mapper

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

如果我正在处理来自外部源的数据 - 例如,User 有许多 Messages - 并且外部源仅提供最新的几个实体(如 RSS 提要),$user->addMessage($message) 如何检查对于重复项,除非它知道 Mapper,或者它在集合中“搜索”(似乎是一种低效的做法)。

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

如果我有一个大型集合 - 又是一个 User 和许多 Messages - User 实体如何在不实际代理 Mapper 调用的情况下为集合提供限制和分页?

同样,控制器或事务脚本或任何使用实体的东西都可以直接使用映射器来检索受计数、日期范围或其他因素限制的UserMessages 的集合——但这也将导致代码重复。

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

【问题讨论】:

    标签: php design-patterns oop orm datamapper


    【解决方案1】:

    规则 #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。您的域实体对象就是那些对象......它们不应该知道它们来自哪里,域模型只是管理它们并将它们传递给管理和操作它们的各种功能。

    回头看,我不确定我是否完全回答了您的问题。但是,我不认为实体本身应该有权访问映射器。创建服务/存储库/对对象进行操作并在这些功能中使用适当技术的任何东西...

    也不要从一开始就过度设计它。让您的领域专注于其目标,并在性能确实成为问题时进行重构。

    【讨论】:

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

    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());
    

    连接是反向的。

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

    【讨论】:

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

    没有。

    原因如下:信任。你不能相信数据会为系统的利益而行动。您只能相信系统会对数据采取行动。这是编程逻辑的基础。

    假设数据中有一些令人讨厌的东西,它是为 XSS 设计的。如果一个数据块正在执行操作或者如果它被评估,那么 XSS 代码就会被混合到事物中并且它会打开一个安全漏洞。

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

    【讨论】:

    • 那么处理我描述的一般情况的最佳方法是什么?只需将其保存在控制器/事务脚本/任何使用实体中?
    • 把它推到一个新的控件类中。将编码视为构建一堆工具总是更好。您永远不会将锤子固定在某人的手臂上-尽管您会给他们机会拿起并使用它。您的数据也是如此。您必须收紧代码,以便它可以根据数据执行某些有效的操作,但它必须受到您的规则的约束。当您让数据控制系统时,您就给了数据破坏系统的机会。
    • 我想我认为实体(或模型)既代表又验证数据。
    • 数据模型无法自我验证。这是违反哲学规律的。参见 Putnam 在增值税思想实验中的大脑:en.wikipedia.org/wiki/Brain_in_a_vat 这意味着数据将假定它是完美的,但如果它在道德上受到损害,它不会知道它已经受到损害(通过设计)。没有系统可以自我评估。这就像建议学生可以为自己的论文评分。只有存在于数据范围之外的系统才能评估数据。希望这对您来说已经足够证明,但我愿意继续辩论是否有帮助。此外,数据必须是可移植的。
    • 我想了更多,我还有一些东西要补充。每当有数据在调节系统功能时,例如设置文件或其他东西,您都可以保留默认值以防出现问题,但您将它们视为经过验证(如果它们有效)来处理。如果用户可以更改它们,那么您要小心处理它们。它与数据不同。你不会以同样的方式处理它。这是选项,经过仔细权衡,他们会修改系统。您仍然不评估它们。您永远不会评估设置文件。但是从用户那里加载系统的全部部分是危险的。不要!:)
    猜你喜欢
    • 1970-01-01
    • 2010-12-30
    • 1970-01-01
    • 2019-09-24
    • 2012-08-06
    • 1970-01-01
    • 2016-10-10
    • 2010-09-17
    • 2013-02-11
    相关资源
    最近更新 更多