【问题标题】:How to properly update a managed entity using data from a non-managed entity?如何使用来自非托管实体的数据正确更新托管实体?
【发布时间】:2015-03-17 20:44:59
【问题描述】:

上下文

我的目标是使用来自同一类但不受 Doctrine 管理的对象的数据对托管实体执行更新。

如果可以在替换属性时执行“简单更新”,这会很酷,但事实上,如果我清理 ArrayCollection,旧数据似乎不会被删除(即使我清理所有引用ArrayCollection 或 orphanRemoval 的元素的小提琴设置为 true)。

但是让我们输入一个具体的例子。我有 this entity 有很多 OneToOne / OneToMany 关系来代表 fiddle。我可以使用 Symfony2 命令导入小提琴样本(以前从另一个环境导出为 json)。

如果示例已经存在,我该如何正确更新它?

坏主意:做 DELETE + INSERT

我使用以下代码(简化)构建我的实体:

$fiddle = new Fiddle();
$fiddle->setHash($this->get($json, 'hash'));
$fiddle->setRevision($this->get($json, 'revision'));

$context = $fiddle->getContext();
$context->setFormat($this->get($json, 'context', 'format'));
$context->setContent($this->get($json, 'context', 'content'));

$fiddle->clearTemplates();
$jsonTemplates = $this->get($json, 'templates') ? : array ();
foreach ($jsonTemplates as $jsonTemplate)
{
    $template = new FiddleTemplate();
    $template->setFilename($this->get($jsonTemplate, 'filename'));
    $template->setContent($this->get($jsonTemplate, 'content'));
    $template->setIsMain($this->get($jsonTemplate, 'is-main'));
    $fiddle->addTemplate($template);
}

// ...

如果我的实体已经存在,我现在可以在删除它后保留它:

    $check = $this
       ->getContainer()
       ->get('doctrine')
       ->getRepository('FuzAppBundle:Fiddle')
       ->getFiddle($fiddle->getHash(), $fiddle->getRevision());

    if (!is_null($check->getId()))
    {
        $em->remove($check);
        $em->flush();
    }

    $em->persist($fiddle);
    $em->flush();

但是如果样本已经存在,这将创建一个 DELETE + INSERT 而不是 UPDATE。这很奇怪,因为用户可以为小提琴添加书签,并且关系是由 id 建立的。

丑陋的想法:对主要实体和 OneToOne 关系执行 UPDATE,并在 OneToMany 关系上执行 DELETE + INSERT

我先得到我的小提琴,如果它已经存在,我清理它并用新数据填充它... 代码运行良好,但真的很丑,你可以检查它here .

作为示例,查看tags 属性:由于标签可能已被删除/更改,我应该正确设置新标签,将旧标签替换为新标签。

// remove the old tags
foreach ($fiddle->getTags() as $tag)
{
   if (\Doctrine\ORM\UnitOfWork::STATE_MANAGED === $em->getUnitOfWork()->getEntityState($tag))
   {
      $em->remove($tag);
      $em->flush();
   }
}

// set the new tags
$tags = new ArrayCollection();
$jsonTags = $this->getFromArray($json, 'tags');
foreach ($jsonTags as $jsonTag)
{
   $tag = new FiddleTag();
   $tag->setTag($jsonTag);
   $tags->add($tag);
}
$fiddle->setTags($tags);

由于使用 fiddle 的 id 引用标签,我可以使用 ->remove,即使这很丑陋。这里没问题,但如果 id 是自动生成的,那么肯定有更好的解决方案。

我也尝试简单地将旧小提琴的 id 设置为新的并合并,但这导致了以下异常:

[Symfony\Component\Debug\Exception\ContextErrorException]  
Notice: Undefined index: 00000000125168f2000000014b64e87f

赏金?

不仅仅是一个简单的“导入功能”,我还想使用这种更新样式将表单绑定到非托管实体,并仅在需要时更新现有实体。所以我的目标是让一些通用的东西适用于所有类型的实体。

但我当然不期望整个代码。处理托管 ArrayCollection 的更新的良好做法,以及在编写此功能之前我应该​​考虑的一些提示/警告就足够了。

【问题讨论】:

  • 我想我对发生的事情感到困惑。为什么不直接获取您的实体,进行更新,然后刷新?你为什么要删除/保留?
  • 我无法轻松进行更新,因为有很多 OneToOne / OneToMany :以包含 ArrayCollection 的 tags 属性为例:也许删除了一些标签并添加了一些其他标签,我需要使用复杂(和丑陋)的代码来进行正确的更新。我使用了更新 here 但正如你所看到的,这是一个用于正确更新对象的巨大代码 - 如果 Doctrine 有它,我正在寻找更自动/更简单的东西。
  • 我只是希望我能更好地理解您的具体问题,或者您通过自动刷新托管实体等的含义。
  • 当我将托管实体绑定到已发布的表单 ($form->handleRequest($fiddle);)` 并且表单正确填写($form->isValid() 返回 true)时,我需要进行一些其他检查以查看是否当前用户能够保存它。如果他被允许保存,没问题;但是如果他没有被允许保存,我应该分离实体以避免它被自动更新。我在 3 天前发现了这个令人难以置信的自动更新,在我完全理解发生了什么之前,我永远不会再使用托管实体来处理表单。
  • 您不应该在表单检查是否有效之前进行安全检查吗?想要检查 @Security 注释?

标签: php symfony doctrine-orm entity


【解决方案1】:

控制 Doctrine 坚持的内容

仅在需要时更新现有实体。

这可以通过 Doctrine 相当简单地实现:

您正在寻找的是变更跟踪政策Deferred Explicit。

Doctrine 默认使用更改跟踪策略Deferred Implicit。这意味着当您调用 $em->flush() 时,Doctrine 将遍历其所有托管实体以计算变更集。然后所有更改都将保留。

当使用更改跟踪策略 Deferred Explicit 并调用 $em->flush() 时,Doctrine 将仅检查您明确调用 $em->persist() 的实体。换句话说:您可以拥有数千个托管实体,其中 2 个称为 $em->persist(),而 Doctrine 只会计算这 2 个的变更集(并在需要时持续更改)。

可以在实体类级别上设置更改跟踪策略。所以如果你想让某个实体类使用Deferred Explicit,只需在类doc-block中添加注解即可:

/**
 * @Entity
 * @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
 */
class Fiddle
{

那么只需要在真正需要的时候拨打$em->persist($fiddle)就可以了。

为整个聚合(根实体及其所有子实体)设置相同的更改跟踪策略可能是明智的。

PS:还有名为Notify 的第三个更改跟踪策略,设置起来需要更多的工作,但在调用$em->flush() 时,您可以更细粒度地控制持久化的内容。但我认为你不需要走这么远。

更新小提琴

看到你用来更新 Fiddle 实体的代码,我认为你可以在那里改进一些东西。

首先将管理关联的责任移回实体:

/**
 * @Entity
 * @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
 */
class Fiddle
{
    // ...

    /**
     * @return FiddleTag[]
     */
    public function getTags()
    {
        return $this->tags->toArray();
    }

    /**
     * @param FiddleTag $tag
     */
    public function addTag(FiddleTag $tag)
    {
        if (!$this->tags->contains($tag)) {
            $this->tags->add($tag);
            $tag->setFiddle($this);
        }
    }

    /**
     * @param FiddleTag $tag
     */
    public function removeTag(FiddleTag $tag)
    {
        if ($this->tags->contains($tag)) {
            $this->tags->removeElement($tag);
            $tag->setFiddle(null);
        }
    }

    /**
     * @param FiddleTag[] $newTags
     */
    public function replaceTags(array $newTags)
    {
        $currentTags = $this->getTags();

        // remove tags that are not in the new list of tags
        foreach ($currentTags as $currentTag) {
            if (!in_array($currentTag, $newTags, true)) {
                $this->removeTag($currentTag);
            }
        }

        // add tags that are not in the current list of tags
        foreach ($newTags as $newTag) {
            if (!in_array($newTag, $currentTags, true)) {
                $this->addTag($newTag);
            }
        }
    }

    // ...
}

现在 ImportCommand 中的代码如下所示:

$jsonTags = $this->getFromArray($json, 'tags');
$newTags  = [];

foreach ($jsonTags as $jsonTag) {
    $tag = $tagRepo->findOneByTag($jsonTag);

    if ($tag === null) {
        $tag = new FiddleTag();
        $tag->setTag($jsonTag);
    }

    $newTags[] = $tag;
}

$fiddle->replaceTags($newTags);

然后当一切正常并且可以持久化时,做:

$em->persist($fiddle);

foreach ($fiddle->getTags() as $tag) {
    $em->persist($tag);
}

$em->flush();

当您在关联上配置cascade=persist 后,您应该能够省去手动保留标签的循环。

专业提示

您可以查看 JMS Serializer 库,以及将其集成到 Symfony 中的 Bundle。

【讨论】:

  • 这是一个非常有趣的答案,感谢分享。我会在几个小时内检查出来。延迟隐式正是我对我意想不到的坚持感到如此惊讶的原因。不错的收获。
猜你喜欢
  • 1970-01-01
  • 2013-02-20
  • 2011-11-18
  • 1970-01-01
  • 1970-01-01
  • 2013-11-13
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多