在Laravel中同步一对多关系

use*_*172 21 php crud one-to-many laravel eloquent

如果我有多对多的关系,那么用它的sync方法更新关系是非常容易的.

但是我会用什么来同步一对多的关系呢?

  • posts:id, name
  • links:id, name, post_id

在这里,每个Post可以有多个Links.

我想将与数据库中特定帖子相关联的链接与输入的链接集合(例如,从我可以添加,删除和修改链接的CRUD表单)同步.

应删除数据库中不存在于我的输入集合中的链接.应更新数据库和输入中存在的链接以反映输入,并且仅在我的输入中存在的链接应作为新记录添加到数据库中.

总结所需的行为:

  • inputArray = true/db = false --- CREATE
  • inputArray = false/db = true --- DELETE
  • inputArray = true/db = true ----更新

luk*_*ter 15

不幸的是,没有sync一对多关系的方法.自己做这件事非常简单.至少如果您没有任何外键引用links.因为那时你可以简单地删除行并再次插入它们.

$links = array(
    new Link(),
    new Link()
);

$post->links()->delete();
$post->links()->saveMany($links);
Run Code Online (Sandbox Code Playgroud)

如果您确实需要更新现有的(无论出于何种原因),您需要完全按照您在问题中描述的内容进行操作.

  • 在自动增量的情况下,主键容量的耗尽不是会更快吗? (4认同)
  • 不要这样做,因为将来您可能需要存储数据透视数据......或者更糟 - 另一个编码器将存储数据透视数据而不知道同步是假的。 (3认同)
  • 如果您只有两个相关模型,则很有用。不幸的是,就我而言,我有 3 个或更多模型取决于记录的 ID - 所以我不能删除它并重新创建。 (3认同)

Jul*_*atl 8

您可以使用UPSERT插入或更新重复键,也可以使用关系。

这意味着您可以将旧数据与新数据进行比较,并使用一个数组,其中包含要更新的数据和要插入到同一查询中的数据。

您也可以删除其他不需要的 id。

这里有一个例子:

    $toSave = [
        [
            'id'=>57,
            'link'=>'...',
            'input'=>'...',
        ],[
            'id'=>58,
            'link'=>'...',
            'input'=>'...',
        ],[
            'id'=>null,
            'link'=>'...',
            'input'=>'...',
        ],
    ];

    // Id of models you wish to keep
    // Keep existing that dont need update
    // And existing that will be updated
    // The query will remove the rest from the related Post
    $toKeep = [56,57,58];


    // We skip id 56 cause its equal to existing
    // We will insert or update the rest

    // Elements in $toSave without Id will be created into the relationship

    $this->$relation()->whereNotIn('id',$toKeep)->delete();

    $this->$relation()->upsert(
        $toSave,            // Data to be created or updated
        ['id'],             // Unique Id Column Key
        ['link','input']    // Columns to be updated in case of duplicate key, insert otherwise
    );
Run Code Online (Sandbox Code Playgroud)

这将创建下一个查询:

delete from
  `links`
where
  `links`.`post_id` = 247
  and `links`.`post_id` is not null
  and `id` not in (56, 57, 58)
Run Code Online (Sandbox Code Playgroud)

和:

insert into
  `links` (`id`, `link`, `input`)
values
  (57, '...', '...'),
  (58, '...', '...'),
  (null, '...', '...')
  on duplicate key update
  `link` = values(`link`),
  `input` = values(`input`)
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您只需 2 个查询即可更新关系的所有元素。例如,如果您有 1,000 个帖子,并且您想要更新所有帖子的所有链接。


ale*_*exw 6

删除和读取相关实体的问题在于,它将破坏您可能对这些子实体具有的任何外键约束。

更好的解决方案是修改Laravel的HasMany关系以包含以下sync方法:

<?php

namespace App\Model\Relations;

use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php
 */
class HasManySyncable extends HasMany
{
    public function sync($data, $deleting = true)
    {
        $changes = [
            'created' => [], 'deleted' => [], 'updated' => [],
        ];

        $relatedKeyName = $this->related->getKeyName();

        // First we need to attach any of the associated models that are not currently
        // in the child entity table. We'll spin through the given IDs, checking to see
        // if they exist in the array of current ones, and if not we will insert.
        $current = $this->newQuery()->pluck(
            $relatedKeyName
        )->all();

        // Separate the submitted data into "update" and "new"
        $updateRows = [];
        $newRows = [];
        foreach ($data as $row) {
            // We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and
            // match a related row in the database.
            if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) {
                $id = $row[$relatedKeyName];
                $updateRows[$id] = $row;
            } else {
                $newRows[] = $row;
            }
        }

        // Next, we'll determine the rows in the database that aren't in the "update" list.
        // These rows will be scheduled for deletion.  Again, we determine based on the relatedKeyName (typically 'id').
        $updateIds = array_keys($updateRows);
        $deleteIds = [];
        foreach ($current as $currentId) {
            if (!in_array($currentId, $updateIds)) {
                $deleteIds[] = $currentId;
            }
        }

        // Delete any non-matching rows
        if ($deleting && count($deleteIds) > 0) {
            $this->getRelated()->destroy($deleteIds);

            $changes['deleted'] = $this->castKeys($deleteIds);
        }

        // Update the updatable rows
        foreach ($updateRows as $id => $row) {
            $this->getRelated()->where($relatedKeyName, $id)
                 ->update($row);
        }

        $changes['updated'] = $this->castKeys($updateIds);

        // Insert the new rows
        $newIds = [];
        foreach ($newRows as $row) {
            $newModel = $this->create($row);
            $newIds[] = $newModel->$relatedKeyName;
        }

        $changes['created'][] = $this->castKeys($newIds);

        return $changes;
    }


    /**
     * Cast the given keys to integers if they are numeric and string otherwise.
     *
     * @param  array  $keys
     * @return array
     */
    protected function castKeys(array $keys)
    {
        return (array) array_map(function ($v) {
            return $this->castKey($v);
        }, $keys);
    }

    /**
     * Cast the given key to an integer if it is numeric.
     *
     * @param  mixed  $key
     * @return mixed
     */
    protected function castKey($key)
    {
        return is_numeric($key) ? (int) $key : (string) $key;
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以重写Eloquent的ModelHasManySyncable而不是标准HasMany关系来使用:

<?php

namespace App\Model;

use App\Model\Relations\HasManySyncable;
use Illuminate\Database\Eloquent\Model;

abstract class MyBaseModel extends Model
{
    /**
     * Overrides the default Eloquent hasMany relationship to return a HasManySyncable.
     *
     * {@inheritDoc}
     * @return \App\Model\Relations\HasManySyncable
     */
    public function hasMany($related, $foreignKey = null, $localKey = null)
    {
        $instance = $this->newRelatedInstance($related);

        $foreignKey = $foreignKey ?: $this->getForeignKey();

        $localKey = $localKey ?: $this->getKeyName();

        return new HasManySyncable(
            $instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
        );
    }
Run Code Online (Sandbox Code Playgroud)

假设您的Post模型扩展MyBaseModel并具有links() hasMany关系,则可以执行以下操作:

$post->links()->sync([
    [
        'id' => 21,
        'name' => "LinkedIn profile"
    ],
    [
        'id' => null,
        'label' => "Personal website"
    ]
]);
Run Code Online (Sandbox Code Playgroud)

此多维数组中具有id与子实体表(links)相匹配的所有记录都将被更新。表中该数组中不存在的记录将被删除。表中不存在的数组中的记录(具有不匹配的id,或者id为null)将被视为“新”记录,并将被插入数据库中。

  • 请注意,RelationShips 是可宏的,因此有一种更简单的方法可以使用 `HasMany::macro( 'sync',syncfunctiongoeshere);` 设置同步函数。 (3认同)
  • 这看起来不错!但由于这是一个旧答案,我想知道它在 Laravel 的新版本中是否仍然可行。经过测试并工作。我将在我的项目中实现这一点。 (2认同)
  • @Ashish 不,它不会影响 laravel 的默认 has-many 关系操作,因为你只是_添加_一个名为sync的新函数到 laravel 的 HasMany 类,并且_不改变_默认的 laravel 代码/行为。 (2认同)
  • @Ashish 哈哈,酷,说实话,我没想到你会回复。实际上,我为将来会引用此答案并与您有同样疑问的人删除了该评论,这样至少他们不会得不到答案。 (2认同)