Si j'ai une relation plusieurs-à-plusieurs, il est super facile de mettre à jour la relation avec sa méthode sync
.
Mais que devrais-je utiliser pour synchroniser une relation un-à-plusieurs?
posts
: id, name
links
: id, name, post_id
Ici, chaque Post
peut avoir plusieurs Link
s.
Je voudrais synchroniser les liens associés à un poste spécifique dans la base de données, avec une collection de liens entrée (par exemple, à partir d'un formulaire CRUD où je peux ajouter, supprimer et modifier des liens).
Les liens de la base de données qui ne sont pas présents dans ma collection d'entrées doivent être supprimés. Les liens qui existent dans la base de données et dans mon entrée doivent être mis à jour pour refléter l'entrée, et les liens qui ne sont présents que dans mon entrée doivent être ajoutés en tant que nouveaux enregistrements dans la base de données.
Pour résumer le comportement souhaité:
Malheureusement, il n'y a pas de méthode sync
pour les relations un-à-plusieurs. C'est assez simple de le faire vous-même. Au moins si vous n'avez pas de clé étrangère référençant links
. Parce qu'alors, vous pouvez simplement supprimer les lignes et les insérer à nouveau.
$links = array(
new Link(),
new Link()
);
$post->links()->delete();
$post->links()->saveMany($links);
Si vous avez vraiment besoin de mettre à jour un existant (pour une raison quelconque), vous devez faire exactement ce que vous avez décrit dans votre question.
Le problème lié à la suppression et à la relecture des entités liées est que cela brisera toutes les contraintes de clé étrangère que vous pourriez avoir sur ces entités enfants.
Une meilleure solution consiste à modifier la relation HasMany
de Laravel pour inclure une méthode 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;
}
}
Vous pouvez remplacer la classe Model
d'Eloquent pour utiliser HasManySyncable
au lieu de la relation standard 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
);
}
En supposant que votre modèle Post
étend MyBaseModel
et a une relation links()
hasMany
, vous pouvez faire quelque chose comme:
$post->links()->sync([
[
'id' => 21,
'name' => "LinkedIn profile"
],
[
'id' => null,
'label' => "Personal website"
]
]);
Tous les enregistrements de ce tableau multidimensionnel qui ont un id
qui correspond à la table d'entité enfant (links
) seront mis à jour. Les enregistrements de la table qui ne sont pas présents dans ce tableau seront supprimés. Les enregistrements du tableau qui ne sont pas présents dans la table (ont un id
ou un id
non nul) seront considérés comme de "nouveaux" enregistrements et seront insérés dans la base de données.
J'ai aimé cela, et c'est optimisé pour une requête minimale et des mises à jour minimales:
tout d'abord, mettez les identifiants des liens à synchroniser dans un tableau: $linkIds
et le modèle de poste dans sa propre variable: $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]);
}