Laravel avoid duplicate entry in child table

Viewed 417

I have product which has many barcodes, when I'm in product edit page and I add/remove/edit barcodes of product it saves barcodes all over again (PS: I am aware where the issue comes from, but not sure about the solution for it)

Logic

So here is what I expect to happen when i'm in product edit page:

  1. If current barcodes has changed just update them
  2. if any of current barcodes removed, delete them from database
  3. if any new barcode added to the list, add it in database

Basically is sync() functionality that i'm looking for, but based on my model relationships i think using sync() is not an option.

Code

Product model

public function barcodes()
{
    return $this->hasMany(Barcode::class, 'product_id', 'id');
}

barcode model

protected $fillable = [
    'product_id', 'serial_number',
];

public function product()
{
    return $this->belongsTo(Product::class);
}

Controller (update function)

public function update(Request $request, $id)
{
    $product = Product::find($id);
    $product->name = $request->input('name');
    if($product->save())
    {
        if(!empty($request->input('barcodes')))
        {
            foreach($request->input('barcodes') as $bb)
            {
                if(!empty($bb['serial_number'])){ // make sure no empty value saves
                    $barcode = new Barcode;
                    $barcode->product_id = $product->id;
                    $barcode->serial_number = $bb['serial_number'];
                    $barcode->save();
                }
            }
        }
    }
}

As I mentioned above I am aware where the issue comes from, but not sure about the solution for it and the issue of duplicate saving data is that I'm using $barcode = new Barcode;.

Extra

In order to be clear, this is what my data looks like when I send update request to back-end

one

Question

What should I change in my code in order to achieve my desire logic?

2 Answers

This is how I create a sync functionality for one to many relationships:

public function update(Request $request, $id)
{
    $product = Product::find($id);
    $product->name = $request->input('name');
    if($product->save())
    {
        if(!empty($request->input('barcodes')))
        {
            $barcodeSync = []; // We will use this to save all the barcode id's that exist in the database.
            foreach($request->input('barcodes') as $bb)
            {
                if(!empty($bb['serial_number'])){
                    $barcode = $product->barcodes()->updateOrCreate(
                        ['id' => $bb['id'] ?? 0],
                        ['serial_number' => $bb['serial_number']]
                    ); // This will create a new barcode for the product if that barcode doesn't exist yet or update the existing barcode using it's id.
                    array_push($barcodeSync, $barcode->id); // This will add the id to the array of existing barcodes for this product.
                }
            }
            $product->barcodes()->whereNotIn('id', $barcodeSync)->delete(); //Deletes all barcodes for this product that were not in the request.
        }
    }
}

This should solve your 3 expectations.

  1. this will update any barcode that matches 'id' and 'product_id'. (note that laravel doesn't actually update the model if the 'serial_number doesn't change).

  2. It saves all the created or updated barcodes for the product in the $barcodeSync array, so you can later delete all barcodes that do not exist in the request anymore $product->barcodes()->whereNotIn('id', $barcodeSync)->delete();

  3. It will create any new barcode passed into the request, since new barcodes don't have 'id' field in the request, it will query id of 0 (0 will not exist), which will trigger the creation of a new barcode and assigning it to the product. (for this to work also make sure the 'id' field of your barcode model is not in the $fillable array because it will try to set an id of 0 every time you create a new barcode, which will result in error)

Hope this helps you!

I think it is because when the data is sent to the server they also contain the old data.

This code always creates a new one and does not update it.

$barcode = new Barcode;
.
.
.
$barcode->save();

Maybe you could something like this.

https://laravel.com/docs/7.x/eloquent#other-creation-methods

foreach($request->input('barcodes') as $bb)
{
  if(!empty($bb['serial_number'])){ // make sure no empty value saves
   /* Update the row where has serial number or create a one */
    Barcode::updateOrCreate([
      'serial_number' => $bb['serial_number'] // if serial number is unique. 
      'product_id'    => $product->id
    ], [ 
      'serial_number' => $bb['serial_number'],
      'product_id'    => $product->id,
    ]);
  }
}

Use serial number to find barcode and update it. When barcode not found create new one.

Hope this helps.

Related