首页 > 解决方案 > belongsToMany: 允许并重用具有现有 UNIQUE 标题的关联条目

问题描述

是)我有的

我的belongsToMany关联类似于CakePHP Cookbook中的关联。但是,我已经UNIQUE对标签标题设置了限制。

(另一个可能无关紧要的区别是,我在Tagssite_id表中的每个标记旁边添加了一个字段,并且在和上都设置了另一个复合约束。)UNIQUEtagsite_id

什么不起作用

提交重复的标签标题会导致错误。

当我在保存之前调试我的新文章实体时,我可以看到重复的标签标题在验证尝试后被拒绝。

'tags' => [
    // This submitted tag title already exists in Tags
    (int) 0 => object(App\Model\Entity\Tag) id:1 {
        'site_id' => (int) 2
        '[new]' => true
        '[accessible]' => [
            'site_id' => true,
            'title' => true,
            'created' => true,
            'modified' => true,
            'site' => true,
            'articles' => true,
        ]
        '[dirty]' => [
            'site_id' => true,
        ]
        '[original]' => [
        ]
        '[virtual]' => [
        ]
        '[hasErrors]' => true
        '[errors]' => [
            'title' => [
                'unique' => 'The provided value is invalid', // ← error
            ],
        ]
        '[invalid]' => [
            'title' => 'test',
        ]
        '[repository]' => 'Tags'
    },

    // …

    // This submitted tag title does *not* already exist in Tags
    (int) 3 => object(App\Model\Entity\Tag) id:4 {
        'title' => 'tag'
        'site_id' => (int) 2
        '[new]' => true
        '[accessible]' => [
            'site_id' => true,
            'title' => true,
            'created' => true,
            'modified' => true,
            'site' => true,
            'articles' => true,
        ]
        '[dirty]' => [
            'title' => true, // ← no error
            'site_id' => true,
        ]
        '[original]' => [
        ]
        '[virtual]' => [
        ]
        '[hasErrors]' => false
        '[errors]' => [
        ]
        '[invalid]' => [
        ]
        '[repository]' => 'Tags'
    },
]

我期望它如何工作?

我正在寻找的行为是如果一个标签已经存在,那么获取它的 ID 并将提交的文章条目链接到该现有 ID。所以ON DUPLICATE KEY从某种意义上说。

是否有一个我缺少的标志可以告诉/允许 ORM 执行此操作,或者我应该开始尝试一些->epilog()技巧?

标签: cakephpormunique-constraintcakephp-4.x

解决方案


ORM保存过程没有这样的功能,不,你不能使用epilog()默认的ORMsave()过程,你必须手动创建插入查询,但是你不能真正使用实体,它不会' t 解决验证问题,您必须或多或少地手动应用验证和应用程序规则(您不想盲目地将数据插入到插入查询中,甚至是强硬的Query::values()绑定数据)。

我可能会建议检查在编组之前修改数据的解决方案是否合适,它将透明地集成到流程中。您可以使用唯一索引列来查找现有行,并将它们的主键值注入请求数据,然后修补/编组过程将能够正确查找现有记录并相应地更新它们。

根据具体的用例,这可能比手动构建插入查询更多工作,但恕我直言,它会更好地集成。在您的特定情况下,它可能更容易,因为使用手动插入查询将需要您分别为所有不同的表插入数据,因为您不能使用 ORM 的关联保存功能和手动构造的插入查询。

最后,这里有一些未经测试的快速和肮脏的示例代码来说明这个概念:

// in ArticlesTable

public function beforeMarshal(
    \Cake\Event\EventInterface $event,
    \ArrayAccess $data,
    \ArrayObject $options
): void {
    // extract lookup keys from request data
    $keys = collection($data['tags'])
        ->extract(function ($row) {
            return [
                $row['tag'],
                $row['site_id'],
            ];
        })
        ->toArray();

    // query possibly existing rows based on the extracted lookup keys
    $query = $this->Tags
        ->find()
        ->select(['id', 'tag', 'site_id'])
        ->where(
            new \Cake\Database\Expression\TupleComparison(
                ['tag', 'site_id'],
                $keys,
                ['string', 'integer'],
                'IN'
            )
        )
        ->disableHydration();

    // create a map of lookup keys and primary keys from the queried rows
    $map = $query
        ->all()
        ->combine(
            function ($row) {
                return $row['tag'] . ';' . $row['site_id'];
            },
            'id'
        )
        ->toArray();

    // inject primary keys based on whether lookup keys exist in the map
    $data['tags'] = collection($data['tags'])
        ->map(function ($row) use ($map) {
            $key = $row['tag'] . ';' . $row['site_id'];
            if (isset($map[$key])) {
                $row['id'] = $map[$key];
            }

            return $row;
        })
        ->toArray();
}

注入现有记录的主键后,编组、验证、规则和保存应该能够正确区分要更新的内容和要插入的内容,即您应该能够像以前一样继续使用默认的 ORM 保存过程.

也可以看看


推荐阅读