Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Synchronizing a one-to-many relationship in Laravel

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

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

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

Here, each Post can have multiple 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).

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
like image 742
user2834172 Avatar asked Nov 29 '14 16:11

user2834172


People also ask

What is hasMany in Laravel?

hasMany relationship in laravel is used to create the relation between two tables. hasMany means create the relation one to Many. For example if a article have comments and we wanted to get all comments of the article then we can use hasMany relationship .

What is sync in Laravel?

01 Use Of Sync In Laravel The sync() method accepts an array as an argument. As the name suggests, this method synchronizes the database entries that means whatever you pass in this method, those records will be kept into the database and the rest will be removed from the intermediate(pivot) table.

What is polymorphic relationship Laravel?

A one-to-one polymorphic relationship is a situation where one model can belong to more than one type of model but on only one association. A typical example of this is featured images on a post and an avatar for a user. The only thing that changes however is how we get the associated model by using morphOne instead.

What is eager loading in Laravel?

Laravel eager loading. What is eager loading? Eager loading is a concept in which when retrieving items, you get all the needed items together with all (or most) related items at the same time. This is in contrast to lazy loading where you only get one item at one go and then retrieve related items only when needed.


2 Answers

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.

like image 127
lukasgeiter Avatar answered Sep 26 '22 19:09

lukasgeiter


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:

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

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

like image 23
alexw Avatar answered Sep 24 '22 19:09

alexw