php 在 Laravel 中同步一对多关系

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27204485/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 23:14:11  来源:igfitidea点击:

Synchronizing a one-to-many relationship in Laravel

phplaraveleloquentone-to-manycrud

提问by user2834172

If I have a many-to-many relationship it's super easy to update the relationship with its syncmethod.

如果我有一个多对多的关系,用它的sync方法更新关系是非常容易的。

But what would I use to synchronize a one-to-many relationship?

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

  • table posts: id, name
  • table links: id, name, post_id
  • postsid, name
  • linksid, name, post_id

Here, each Postcan have multiple Links.

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

I'd like to synchronize the links associated with a specific post in the database, against an inputted collection of links (for example, from a CRUD form where I can add, remove, and modify links).

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

Links in the database that aren't present in my input collection should be removed. Links that exist in the database and in my input should be updated to reflect the input, and links that are only present in my input should be added as new records in the database.

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

To summarize the desired behavior:

总结所需的行为:

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

回答by lukasgeiter

Unfortunately there is no syncmethod for one-to-many relations. It's pretty simple to do it by yourself. At least if you don't have any foreign key referencing links. Because then you can simple delete the rows and insert them all again.

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

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

$post->links()->delete();
$post->links()->saveMany($links);

If you really need to update existing one (for whatever reason) you need to do exactly what you described in your question.

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

回答by alexw

The problem with deleting and readding the related entities, is that it will break any foreign key constraints you might have on those child entities.

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

A better solution is to modify Laravel's HasManyrelationship to include a syncmethod:

更好的解决方案是修改 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;
    }
}

You can override Eloquent's Modelclass to use HasManySyncableinstead of the standard HasManyrelationship:

你可以覆盖 Eloquent 的Model类来HasManySyncable代替标准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
        );
    }

Supposing that your Postmodel extends MyBaseModeland has a links()hasManyrelationship, you can do something like:

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

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

Any records in this multidimensional array that have an idthat matches the child entity table (links) will be updated. Records in the table that are not present in this array will be removed. Records in the array that are not present in the table (Have a non-matching id, or an idof null) will be considered "new" records and will be inserted into the database.

此多维数组中具有id与子实体表 ( links)匹配的任何记录都将被更新。表中不存在于该数组中的记录将被删除。数组中不存在于表中的记录(具有不匹配idid空值)将被视为“新”记录并将被插入到数据库中。

回答by Luca C.

I did like this, and it is optimized for minimal query and minimal updates:

我确实喜欢这个,它针对最少的查询和最少的更新进行了优化

first, put link ids to sync in an array: $linkIdsand the post model in its own variable: $post

首先,将要同步的链接 ID 放入一个数组中:$linkIds并将 post 模型放入其自己的变量中:$post

Link::where('post_id','=',$post->id)->whereNotIn('id',$linkIds)//only remove unmatching
    ->update(['post_id'=>null]);
if($linkIds){//If links are empty the second query is useless
    Link::whereRaw('(post_id is null OR post_id<>'.$post->id.')')//Don't update already matching, I am using Raw to avoid a nested or, you can use nested OR
        ->whereIn('id',$linkIds)->update(['post_id'=>$post->id]);
}