Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Soft deleting / detaching and restoring / attaching relationships with composite keys

I have 2 models that are joined by a relationship which has a composite key - these are Product and Category. I need to use soft deletes on all tables, so that the models and relationships can be restored if required.

In my Product model I have:

function categories()
{
    return $this->belongsToMany('App\Category', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
}

In my Category model I have:

function products()
{
    return $this->belongsToMany('App\Product', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
}

I read elsewhere about chaining the whereNull method, as queries like $category->products->contains($product->id) were otherwise returning the soft deleted relationships.

My question is what is the best way to handle deleting and restoring these soft deleted relationships? For restoring, for example, I tried:

$product->categories()->restore($category_id);

The above produced an SQL error saying the deleted_at field was ambiguous (because it joined the categories table to product_categories).

Update - It appears the root issue is that the BelongsToMany class does not support soft deletes - so attach, detach and sync all perform hard deletes. What would be the best approach to overriding this class?

like image 262
BrynJ Avatar asked Jul 08 '15 16:07

BrynJ


2 Answers

Basically, there will be only one deleted_at field and instead of using the $product->categories() use two custom (common) methods in both (Product and Category) models for example, you may create a trait like this:

// SoftDeletePC.php
trait SoftDeletePC {
    // SoftDelete
    public function softDeleteProductCategory($productId, $categoryId)
    {
        \DB::table('product_categories')
        ->where('product_id', $productId)
        ->where('category_id', $categoryId)
        ->update(['deleted_at' => \DB::raw('NOW()')]);
    }

    // Restore
    public function restoreProductCategory($productId, $categoryId)
    {
        \DB::table('product_categories')
        ->where('product_id', $productId)
        ->where('category_id', $categoryId)
        ->update(['deleted_at' => null]);
    }
}

Then use this trait in both models using use TraitProductCategory and call the method from both models for example:

// App/Product.php
class product extends Model {
    use SoftDeletePC;
}

// App/Category.php
class Category extends Model {
    use SoftDeletePC;
}

So, instead of using this:

Product->find(1)->categories()->restore(2);

You may use something like this:

$product = Product->find(1);

$product->softDeleteProductCategory(1, 2); // Set timestamp to deleted_at

$product->restoreProductCategory(1, 2); // Set null to deleted_at

Hope this may work for you.

like image 121
The Alpha Avatar answered Oct 22 '22 04:10

The Alpha


I have ended up creating some custom methods in my Product model to accomplish what I require - not my ideal solution, but it works nevertheless. My custom sync looks like this:

class Product extends Model
{
    // update relationship to categories
    function categories_sync($category_ids)
    {
        // categories
        $existing_category_ids = $this->categories()->lists('category_id')->all();
        $trashed_category_ids = $this->categories('onlyTrashed')->lists('category_id')->all();

        if(is_array($category_ids)) {

            foreach($category_ids as $category_id) {
                if(in_array($category_id, $trashed_category_ids)) {
                    $this->categories()->updateExistingPivot($category_id, ['deleted_at' => null]);
                }
                elseif(!in_array($category_id, $existing_category_ids)) {
                    $this->categories()->attach($category_id);
                }
            }

            foreach($existing_category_ids as $category_id) {
                if(!in_array($category_id, $category_ids)) {
                    $this->categories()->updateExistingPivot($category_id, ['deleted_at' => date('YmdHis')]);
                }
            }
        }
        else {
            foreach($existing_category_ids as $category_id) {
                $this->categories()->updateExistingPivot($category_id, ['deleted_at' => date('YmdHis')]);
            }
        }
    }
}

The above relies upon an extended categories() method:

// relationship to categories
function categories($trashed=false)
{
    if($trashed=='withTrashed') {
        return $this->belongsToMany('App\Category', 'product_categories')->withTimestamps();
    }
    elseif($trashed=='onlyTrashed') {
        return $this->belongsToMany('App\Category', 'product_categories')->whereNotNull('product_categories.deleted_at')->withTimestamps();
    }
    else {
        return $this->belongsToMany('App\Category', 'product_categories')->whereNull('product_categories.deleted_at')->withTimestamps();
    }
}
like image 23
BrynJ Avatar answered Oct 22 '22 03:10

BrynJ