One-to-Many Synchronization in Laravel

If I have a many-to-many relationship, it is very easy to update the connection using the sync method.

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

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

Here, each Post can have multiple Link s.

I would like to synchronize the links associated with a specific message in the database with the entered set of links (for example, from the CRUD form, where I can add, delete and modify links).

Links in the database that are not in my input collection should be deleted. Links that exist in the database and at my entrance should be updated to reflect the input, and links that are only at my entrance should be added as new entries in the database.

To summarize the desired behavior:

  • inputArray = true / db = false --- CREATE
  • inputArray = false / db = true --- DELETE
  • inputArray = true / db = true ---- UPDATE
+6
source share
3 answers

Unfortunately, there is no sync method for a one-to-many relationship. It is quite simple to do it yourself. At least if you do not have a link to the foreign key links . Because then you can just delete the lines and insert them again.

 $links = array( new Link(), new Link() ); $post->links()->delete(); $post->links()->saveMany($links); 

If you really need to update an existing one (for some reason), you need to do exactly what you described in your question.

+6
source

The problem with deleting and reading related objects is that it will violate any foreign key restrictions that you may have on these child objects.

The best solution is to change the relationship of Laravel HasMany to the 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 the Eloquent Model class to use HasManySyncable instead of the standard HasMany relation:

 <?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 ); } 

Suppose your Post model extends MyBaseModel and has links() HasMany , you can do something like:

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

All records in this multidimensional array that have id that correspond to the table of child entities ( links ) will be updated. Entries in the table that are not in this array will be deleted. Entries in the array that are not present in the table (have an id or id of null mismatch) will be considered β€œnew” entries and will be inserted into the database.

0
source

I liked it and it is optimized for minimal query and minimal updates :

first put the sync link ids in the array: $linkIds and the post model in your 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]); } 
0
source

Source: https://habr.com/ru/post/978869/


All Articles