是否有可能将many_many关系版本化?

时间:2013-07-27 10:41:37

标签: silverstripe

我已经在DataObjects上使用了包含大量内容的版本控制,现在我想知道是否可以将版本控制应用于many_many关系?

假设我有以下内容:

class Page extends SiteTree
{
    private static $many_many = array(
        'Images' => 'Image' 
    );
}

然后ORM将为我创建一个Page_Images表来存储关系。为了拥有版本化关系,需要更多的表(例如Page_Images_Live)。

有没有办法告诉ORM创建版本化关系?在查看具有Page * – * Images关系的上述示例时,我希望Image类被版本化,而是关系。例如。像这样的东西:

Version Stage:
---
    PageA
        Images ( ImageA, ImageB, ImageC )

Version Live:
---
    PageA
        Images ( ImageA, ImageC, ImageD, ImageE )

甚至可以开箱即用吗?

2 个答案:

答案 0 :(得分:5)

我花了很多时间研究这个并且没有从根本上修改ManyManyList(因为它没有通过扩展系统暴露必要的钩子),所以没有多少选择。

我是一个甜点第一类的人,我们怎么做呢?

我完成此专长的唯一建议实际上是一个多对多的桥对象(即通过Page加入Image$has_many的单独实体),尽管它仍然需要相当一点修改。

这是partially discussed on the forum,其中有关通过将版本化项目存储在实际对象而不是连接表中来破坏实际关系的解决方案。这可行,但我认为我们仍然可以做得更好。

我个人倾向于将关系的版本与Page本身联系起来,下面的部分解决方案涵盖了这一点。请阅读以下内容,了解更多信息,将其作为ManyManyList的更新。

这样的事情是一个开始:

class PageImageVersion extends DataObject
{
    private static $db = array(
        'Version' => 'Int'
    );

    private static $has_one = array(
        'Page' => 'Page',
        'Image' => 'Image'
    );
}

这包含我们的双向关系以及我们存储的版本号。您需要指定getCMSFields函数以添加所需的正确字段,以允许您将其与现有图像相关联或上传新图像。我避免覆盖它,因为它与实际的版本处理部分相比应该相对简单。

现在,我们在has_many上有一个Page,如此:

private static $has_many = array(
    'Images' => 'PageImageVersion' 
);

在我的测试中,我还为Image添加了一个扩展程序,将匹配的$has_many添加到其中,如下所示:

class ImageExtension extends DataExtension
{
    private static $has_many = array(
        'Pages' => 'PageImageVersion'
    );
}
  

老实说,除了添加Pages之外,还不确定是否有必要这样做   在关系的Image方面起作用。据我所知,对于这个特殊的用例来说,它真的很重要。

不幸的是,由于这种版本控制方式,我们无法使用调用Images的标准方式,我们需要有点创意。像这样:

public function getVersionedImages($Version = null)
{
    if ($Version == null)
    {
        $Version = $this->Version;
    }
    else if ($Version < 0)
    {
        $Version = max($this->Version - $Version, 1);
    }

    return $this->Images()->filter(array('Version' => $Version));
}

当您致电getVersionedImages()时,它会返回所有设置了Version的图片,并与当前页面的版本保持一致。还支持通过getVersionedImages(-1)获取最新版本的先前版本,或者甚至通过传递任何位置编号来获取特定版本页面的图像。

好的,到目前为止一切顺利。我们现在需要确保每个页面都写入我们为这个新版本的页面获取重复的图像列表。

onAfterWrite上使用Page功能,我们可以执行此操作:

public function onAfterWrite()
{
    $lastVersionImages = $this->getVersionedImages(-1);
    foreach ($lastVersionImages as $image)
    {
        $duplicate = $image->duplicate(false);
        $duplicate->Version = $this->Version;
        $duplicate->write();
    }
}
  

对于那些在家里玩的人来说,这就是恢复以前版本的Page会如何影响这一点的问题。

因为我们会在GridField中对其进行编辑,所以我们需要做一些事情。首先是确保我们的代码可以处理Add New函数。

我的想法是onAfterWrite对象上的PageImageVersion

public function onAfterWrite()
{
    //Make sure the version is actually saved
    if ($this->Version == 0)
    {
        $this->Version = $this->Page()->Version;
        $this->write();
    }
}

要让您的版本化项目显示在GridField中,您可以将其设置为与此类似:

$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridField = new GridField("Images", "Images", $this->getVersionedImages(), $gridFieldConfig);
$fields->addFieldToTab("Root.Images", $gridField);

您可能希望直接从GridField通过GridFieldConfig_RelationEditor链接到图片,但这是事情变得糟糕的时候。

蔬菜的时间......

最大的困难之一是GridField,用于链接和取消链接这些实体。使用标准GridFieldDeleteAction将直接更新没有正确版本的关系。

您需要扩展GridFieldDeleteAction并覆盖handleAction以编写Page对象(以触发其他版本),复制我们版本化图像对象的最新版本,同时让它跳过你不想要的新版本。

  

我承认,最后一点只是我的猜测。根据我的理解和调试,它应该可以工作,但只是有很多摆弄才能使它正确。

然后,您的GridFieldDeleteAction扩展名需要添加到您的特定GridField

这基本上是您使该解决方案有效的最后一步。一旦你将添加,删除,复制,版本更新部分缩小,只需使用getVersionedImages()来获取正确的图像。

结论

避免。我明白为什么你要这样做,但我真的没有看到一种干净的方式来处理这个问题而没有像many_many这样的大小更新关系在Silverstripe处理。


但我真的希望它为ManyManyList

ManyManyList所需的更改包含3向密钥(外键,本地密钥,版本密钥)以及添加/删除/获取等各种更新方法。

如果addremove函数中存在挂钩,您可以将该功能作为扩展(通过Silverstripe的扩展系统)隐藏,并将所需数据添加到many_many关系允许的额外字段。

虽然我可以通过直接扩展ManyManyList然后强制ManyManyList通过Object::useCustomClass替换为我的自定义类来实现这一点,但它会是均匀的更多的是一个混乱的解决方案。

对于我来说,在这个阶段给出一个纯粹的ManyManyList解决方案的完整答案对我来说太长/太复杂了(尽管我可能会稍后再回过头来试一试)。


免责声明:我不是Silverstripe Core开发者,可能有一个更整洁的解决方案,但我根本无法看到。

答案 1 :(得分:1)

您可以使用“_Live”后缀定义第二个关系,并在发布页面时更新它。注意:此解决方案仅存储两个版本(实时和阶段)。

Bellow是我的实现,可以自动检测是否有很多关系版本化。然后它处理发布和数据检索。所需要的只是用“_Live”后缀定义一个额外的多对多关系。

$ page-&gt; Images()根据当前阶段(阶段/现场)返回项目。

class Page extends SiteTree
{
    private static $many_many = array(
        'Images' => 'Image',
        'Images_Live' => 'Image'
    );

    public function publish($fromStage, $toStage, $createNewVersion = false)
    {
        if ($toStage == 'Live')
        {
            $this->publishManyToManyComponents();
        }

        parent::publish($fromStage, $toStage, $createNewVersion);
    }

    protected function publishManyToManyComponents()
    {
        foreach (static::getVersionedManyManyComponentNames() as $component_name)
        {
            $this->publishManyToManyComponent($component_name);
        }
    }

    protected function publishManyToManyComponent($component_name)
    {
        $stage = $this->getManyManyComponents($component_name);
        $live = $this->getManyManyComponents("{$component_name}_Live");

        $live_table = $live->getJoinTable();
        $live_fk = $live->getForeignKey();
        $live_lk = $live->getLocalKey();

        $stage_table = $stage->getJoinTable();
        $stage_fk = $live->getForeignKey();
        $stage_lk = $live->getLocalKey();

        // update or add items from stage to live
        foreach ($stage as $item)
        {
            $live->add($item, $stage->getExtraData(null, $item->ID));
        }

        // delete remaining items from live table
        DB::query("DELETE l FROM $live_table AS l LEFT JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk WHERE s.ID IS NULL");

        // update new items IDs in live table (IDs are incremental so the new records can only have higher IDs than items in ID => should not cause duplicate IDs)
        DB::query("UPDATE $live_table AS l INNER JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk SET l.ID = s.ID WHERE l.ID != s.ID;");
    }

    public function manyManyComponent($component_name)
    {
        if (Versioned::current_stage() == 'Live' && static::isVersionedManyManyComponent($component_name))
        {
            return parent::manyManyComponent("{$component_name}_Live");
        }
        else
        {
            return parent::manyManyComponent($component_name);
        }
    }

    protected static function isVersionedManyManyComponent($component_name)
    {
        $many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
        return isset($many_many_components[$component_name]) && isset($many_many_components["{$component_name}_Live"]);
    }

    protected static function getVersionedManyManyComponentNames()
    {
        $many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);

        foreach ($many_many_components as $component_name => $dummy)
        {
            $is_live = 0;

            $stage_component_name = preg_replace('/_Live$/', '', $component_name, -1, $is_live);

            if ($is_live > 0 && isset($many_many_components[$stage_component_name]))
            {
                yield $stage_component_name;
            }
        }
    }
}