Синхронизация отношений один-ко-многим в Laravel

Если у меня есть отношения многие ко многим, это очень легко обновить отношения с его sync метод.

Но что бы я использовал для синхронизации отношений один ко многим?

  • Таблица posts: id, name
  • Таблица links: id, name, post_id

Здесь каждый Post может иметь несколько Links.

Я хотел бы синхронизировать ссылки, связанные с определенным сообщением в базе данных, с введенным набором ссылок (например, из формы CRUD, где я могу добавлять, удалять и изменять ссылки).

Ссылки в базе данных, которых нет в моей коллекции входных данных, должны быть удалены. Ссылки, которые существуют в базе данных и на моем входе, должны обновляться, чтобы отражать входные данные, а ссылки, которые присутствуют только на моем входе, должны добавляться как новые записи в базе данных.

Подводя итог желаемого поведения:

  • inputArray = true / db = false — CREATE
  • inputArray = false / db = true — УДАЛИТЬ
  • inputArray = true / db = true —- ОБНОВЛЕНИЕ

13

Решение

К сожалению, нет sync метод отношений один ко многим. Это довольно просто сделать самостоятельно. По крайней мере, если у вас нет ссылок на внешние ключи links, Потому что тогда вы можете просто удалить строки и вставить их все снова.

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

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

Если вам действительно необходимо обновить существующую (по какой-либо причине), вам нужно сделать именно то, что вы описали в своем вопросе.

12

Другие решения

Проблема с удалением и чтением связанных сущностей заключается в том, что это нарушит все ограничения внешнего ключа, которые могут быть у этих дочерних сущностей.

Лучшее решение — модифицировать Laravel’s. 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;
}
}

Вы можете переопределить Eloquent’s 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
);
}

Предположим, что ваш Post модель расширяется MyBaseModel и имеет links() hasMany отношения, вы можете сделать что-то вроде:

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

Любые записи в этом многомерном массиве, которые имеют id соответствует таблице дочерних сущностей (links) будет обновлено. Записи в таблице, которых нет в этом массиве, будут удалены. Записи в массиве, которых нет в таблице (имеют несоответствие idили id из нуля) будут считаться «новыми» записями и будут вставлены в базу данных.

3

Я так и сделал, и это оптимизирован для минимального запроса и минимальных обновлений:

во-первых, поместите идентификаторы ссылок для синхронизации в массиве: $linkIds и модель поста в своей собственной переменной: $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]);
}
0