12

我已经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 )

这甚至可以开箱即用吗?

4

2 回答 2

5

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

我是甜点第一的人,怎么办?

我完成这一壮举的唯一建议本质上是一个多对多的桥对象(即一个单独的实体连接PageImage),$has_many尽管它仍然需要相当多的修改。

在论坛上进行了部分讨论,其中通过将版本化项目存储在实际对象而不是连接表中来破坏实际关系的解决方案。这会奏效,但我认为我们仍然可以做得更好。

我个人倾向于将关系的版本与Page自身联系起来,下面的部分解决方案涵盖了这一点。阅读首屏以获取更多信息,尝试将此作为ManyManyList.

这样的事情是一个开始:

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

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

这包含我们的 2-way 关系以及我们存储的版本号。您将需要指定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)最新版本获取以前的版本,甚至通过传递任何位置编号来获取页面特定版本的图像。

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

使用onAfterWriteon 函数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);

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

蔬菜的时间...

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

您将需要扩展GridFieldDeleteAction和覆盖handleAction以编写您的Page对象(以触发另一个版本),为最后一个版本复制我们版本化图像对象的每个版本,同时使其跳过新版本中您不想要的那个。

我承认,这最后一点只是我的猜测。根据我的理解和调试,它应该可以工作,但要让它正确,需要做很多工作。

您的 then 扩展名GridFieldDeleteAction需要添加到您的特定GridField.

这基本上是您远离使该解决方案发挥作用的最后一步。一旦你完成了添加、删除、复制、版本更新部分,这真的只是使用getVersionedImages()来获得正确的图像的问题。

结论

避免。many_many我明白你为什么要这样做,但如果没有对 Silverstripe 中处理关系的方式进行相当大的更新,我真的看不出有一种干净的方式能够处理这个问题。


但我真的很想把它当作一个ManyManyList

我看到所需的更改ManyManyList是具有 3 向密钥(外键、本地密钥、版本密钥)和用于添加/删除/获取等的各种方法已更新。

add如果and函数中有钩子remove,您也许可以将功能作为扩展(通过 Silverstripe 的扩展系统)潜入其中,并将所需的数据添加到many_many关系允许的额外字段中。

虽然我可以通过直接扩展ManyManyList然后通过强制ManyManyList替换为我的自定义类来实现这种情况Object::useCustomClass,但这将是一个更加混乱的解决方案。

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


免责声明:我不是 Silverstripe Core 开发人员,可能有一个更简洁的解决方案来解决整个问题,但我根本看不出如何。

于 2015-04-19T07:38:54.540 回答
1

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

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

$page->Images() 根据当前阶段(stage/live)返回项目。

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;
            }
        }
    }
}
于 2016-11-23T15:09:46.877 回答