Synchronizing a one-to-many relationship in Laravel Synchronizing a one-to-many relationship in Laravel php php

Synchronizing a one-to-many relationship in Laravel


Unfortunately there is no sync method 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.

$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.


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 HasMany relationship to include a sync method:

<?phpnamespace 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 Model class to use HasManySyncable instead of the standard HasMany relationship:

<?phpnamespace 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 Post model extends MyBaseModel and has a links() hasMany relationship, you can do something like:

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

Any records in this multidimensional array that have an id that 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 id of null) will be considered "new" records and will be inserted into the database.


You can use UPSERT to insert or update on duplicate key, also using relations.

That means that you can comper your old data with your new data, and use an array with the data to be updated with the data to be inserted in the same query.

Also you can delete other ids that are not needed.

Here an example:

    $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    );

That will create the next queries:

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

And:

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

This is how you can update all elements of a relationship in just 2 queries. For example if you have 1,000 Posts, and you want to update all the links of all the posts.