web-dev-qa-db-fra.com

Synchronisation d'une relation un-à-plusieurs dans Laravel

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?

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

Ici, chaque Post peut avoir plusieurs Links.

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é:

  • inputArray = true/db = false --- CREATE
  • inputArray = false/db = true --- DELETE
  • inputArray = true/db = true ---- UPDATE
22
user2834172

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.

19
lukasgeiter

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.

8
alexw

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]);
}
1
Luca C.