belongsToMany:允许并重用具有现有唯一标题的关联条目

belongsToMany: allow and reuse an associated entry with existing UNIQUE title

我有什么

我的 belongsToMany 协会与 CakePHP Cookbook 的协会相似。但是,我在标签标题上设置了 UNIQUE 约束。

(另一个可能无关紧要的区别是,我在 Tags table 中的每个标签旁边添加了一个 site_id 字段,另一个在 tagsite_id 上都设置了复合 UNIQUE 约束。)

什么不起作用

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

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

'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,然后 link 将提交的文章条目 link 到该现有 ID。所以 ON DUPLICATE KEY 子句,在某种程度上。

是否有一个我遗漏的标志 tell/allow ORM 可以执行此操作,或者我是否应该开始尝试一些 ->epilog() 技巧?

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

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

根据具体用例,这可能比手动构建插入查询需要更多工作,但恕我直言,它会集成得更好。在您的特定情况下,它可能更容易,因为使用手动插入查询需要您分别为所有不同的表插入数据,因为您不能将 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 保存过程,就像你习惯了。

另见