doctrine2额外的懒惰提取关联

时间:2014-07-01 17:18:47

标签: php doctrine-orm

我有一个Thread实体与OneToMany实体有Message个关联。我正在使用DQL查询获取一个线程,我想将其消息量限制为10.因此,我将获取模式设置为EXTRA_LAZY,如下所示。

class Thread
{
    // ...

    /**
     * @var ArrayCollection
     * @ORM\OneToMany(targetEntity="Profile\Entity\Message", mappedBy="thread", fetch="EXTRA_LAZY")
     * @ORM\OrderBy({"timeSent" = "ASC"})
     */
    protected $messages;
}

这允许我使用slice方法向数据库发出LIMIT SQL查询。到目前为止都很好。因为我的消息是加密的,所以在将线程对象处理到控制器(并最终查看)之前,我需要在服务层中对它们进行解密。为实现这一目标,我在服务中执行以下操作:

foreach ($thread->getMessages()->slice(0, 10) as $message) {
    // Decrypt message
}

slice的调用会触发一个提取10条消息的SQL查询。在我看来,我正在做以下事情来呈现线程的消息:

$this->partialLoop()->setObjectKey('message');
echo $this->partialLoop('partial/thread/message.phtml', $thread->getMessages());

问题是这会从数据库中获取整个消息集合。如果我在服务中调用slice,则会向数据库发出带LIMIT 10的相同SQL查询,这是不可取的。

如何在我的服务层中处理有限的邮件集合而不在我的视图中发出另一个SQL查询?也就是说,要让doctrine创建单个SQL查询,而不是两个。我可以简单地在我的视图中解密我的消息,但在这种情况下,这种方式无法实现服务层的目的。我当然可以“手动”获取消息并将它们添加到线程对象中,但如果我可以通过关联自动完成,那么这将是首选。

提前致谢!

4 个答案:

答案 0 :(得分:5)

与大多数人建议的方法略有不同:

<强>切片

Thread实体中,有一个返回消息片段的专用方法:

class Thread
{
    // ...

    /**
     * @param  int      $offset
     * @param  int|null $length
     * @return array
     */
    public function getSliceOfMessages($offset, $length = null)
    {
        return $this->messages->slice($offset, $length);
    }
}

这样可以轻松检索视图中的切片,而不会有获取整个集合的风险。

解密邮件内容

接下来,您需要解密的邮件内容。

我建议你创建一个可以处理加密/解密的服务,让Message实体依赖它。

class Message
{
    // ...

    /**
     * @var CryptService
     */
    protected $crypt;

    /**
     * @param CryptService $crypt
     */
    public function __construct(CryptService $crypt)
    {
        $this->crypt = $crypt;
    }
}

现在您必须通过向Message传递CryptService来创建Message个实体。您可以在创建Message实体的服务中对其进行管理。

但这只会照顾实例化的PostLoad个实体,而不是那些 Doctrine 实例化的实体。为此,您可以使用class SetCryptServiceOnMessageListener { /** * @var CryptService */ protected $crypt; /** * @param CryptService $crypt */ public function __construct(CryptService $crypt) { $this->crypt = $crypt; } /** * @param LifecycleEventArgs $event */ public function postLoad(LifecycleEventArgs $event) { $entity = $args->getObject(); if ($entity instanceof Message) { $message->setCryptService($this->crypt); } } } 事件。

创建一个事件监听器:

CryptService

只要Doctrine加载一个事件,该事件监听器就会向Message实体注入一个$eventListener = new SetCryptServiceOnMessageListener($crypt); $eventManager = $entityManager->getEventManager(); $eventManager->addEventListener(array(Events::postLoad), $eventListener);

在应用程序的bootstrap / configuration阶段注册事件监听器:

Message

将setter添加到class Message { // ... /** * @param CryptService $crypt */ public function setCryptService(CryptService $crypt) { if ($this->crypt !== null) { throw new \RuntimeException('We already have a crypt service, you cannot swap it.'); } $this->crypt = $crypt; } } 实体:

CryptService

正如您所看到的,setter可以防止交换Message(您只需要在没有时设置它)。

现在,CryptService实体将始终具有CryptService作为依赖关系,无论您或Doctrine是否实例化它!

最后,我们可以使用class Message { // ... /** * @param string $content */ public function setContent($content) { $this->content = $this->crypt->encrypt($content); } /** * @return string */ public function getContent() { return $this->crypt->decrypt($this->content); } } 加密和解密内容:

foreach ($thread->getSliceOfMessages(0, 10) as $message) {
    echo $message->getContent();
}

<强>用法

在视图中,您可以执行以下操作:

Message

你可以看到这很简单!

另一位专业人士认为,内容只能以加密形式存在于{{1}}实体中。您永远不会意外地将未加密的内容存储在数据库中。

答案 1 :(得分:3)

slice方法的评论说:

  

调用此方法将仅返回选定的切片,并且不会更改调用集合切片中包含的元素。

因此,调用slice对您的PersistentCollection方法返回的全局getMessages没有影响:我认为您尝试在此处实现的目标是可行的。

作为一种变通方法,您可以在$availableMessages类中声明Thread属性,表示该属性未映射到Doctrine。它看起来像是:

class Thread {
    /**
     * @var ArrayCollection
    */
    protected $availableMessages;
    ...
    public function __construct() {
        ...
        $this->availableMessages = new ArrayCollection();
    }
    ...
    public function getAvailableMessages() {
        return $this->availableMessages;
    }
    public function addAvailableMessage($m) {
        $this->availableMessages->add($m);
    }
    ...
}

当您在服务中处理邮件时,您可以:

$messages = $thread->getMessages()->slice(0, 10);
foreach ($messages as $message) {
    //do your process...
    ...
    //add the unpacked message to the proper "placeholder"
    $thread->addAvailableMessage($message);
}

然后在你看来:

echo $this->partialLoop(
    'partial/thread/message.phtml',
    $thread->getAvailableMessages()
);

您的实施可能会有一些差异,例如您可能更喜欢使用ID索引数组而不是ArrayCollection $availableMessages,和/或使用set代替add方法,但你明白了......

无论如何,这个逻辑允许你控制来自服务层的输出消息量,而不会影响后面调用的层,这就是我所理解的:)

希望这有帮助!

答案 2 :(得分:1)

我相信您应该为解密的邮件设置不同的类,并将它们分别推送到您的视图中。因为Message实际上是你的实体模型,应该用于将数据映射到你的数据库,但你的目的是完全不同的。

因为我从你的代码中了解到你做了什么。像:

$message->setText(decrypt($message->getText));

之后你会变得更糟

$thread->setMessages(new ArrayCollection($thread->getMessages()->slice(0, 10)));

想象一下,在您进行此更改后,代码$em->flush()中的某处发生了。 除了那些10之外,你将丢失所有信息,并且它们将被解密存储。

因此,您可以将它们作为单独的解密消息数组(如果此视图中只有一个线程)或将ThreadID作为第一级的多维消息数组推送。

或者你可以实现Stock Overflaw给你的答案。

它取决于你,但你当然应该改变你的方法。

根据我的观点,最好的解决方案是将解密实现为ViewHelper。

答案 3 :(得分:0)

好像我弄清楚了。在调用slice时,我得到了一组实体。如果我将此数组转换为\Doctrine\Common\Collections\ArrayCollection的实例并调用setMessages方法,它似乎可以正常工作。

$messages = $thread->getMessages()->slice(0, 10);

foreach ($messages as $message) {
    // Decrypt message
}

$thread->setMessages(new ArrayCollection($messages));

然后,在我看来,我可以像往常一样打电话给$thread->getMessages()。这样做的好处是我的视图不需要知道任何解密正在发生或知道任何其他属性。结果是只执行了一个SQL查询,这是一个“由slice生成的” - 正是我想要的。