kirkbushell / eloquence

A drop-in library for certain database functionality in Laravel, that allows for extra features that may never make it into the main project.
MIT License
537 stars 60 forks source link

CountCache on BelongsToMany #106

Open Xety opened 11 months ago

Xety commented 11 months ago

Hello,

Is there a way to do a countcache with a BelongsToMany relationship with a pivot table ? There's no info in the doc

kirkbushell commented 10 months ago

Not yet, but I think it'll be possible with version 11.

kirkbushell commented 10 months ago

@Xety took another look at this, this is going to be considerably more difficult than I thought. I'll pen it for an 11.1 release and work on it in the background. I think I need to essentially create solutions for each type of relation, rather than eloquence feature.

Xety commented 7 months ago

Hey, I have tested some things about this and i found something interesting. I'm using a Model Pivot like this one :

<?php

namespace App\Models;

use Eloquence\Behaviours\CountCache\Countable;
use Illuminate\Database\Eloquent\Relations\Pivot;

class MaterialPart extends Pivot
{
    use Countable;

    /**
     * Return the count cache configuration.
     *
     * @return array
     */
    public function countCaches(): array
    {
        return [
            [
                'model'      => Material::class,
                'field'      => 'part_count',
                'foreignKey' => 'material_id',
                'key'        => 'id'
            ],
            [
                'model'      => Part::class,
                'field'      => 'material_count',
                'foreignKey' => 'part_id',
                'key'        => 'id'
            ]
        ];
    }

}

So my Materials can have many Parts, and a Parts can have many Materials (belongsToMany). In Material model :

/**
 * Get the parts for the material.
 *
 * @return BelongsToMany
 */
public function parts(): BelongsToMany
{
    return $this->belongsToMany(Part::class)
        ->using(MaterialPart::class);
}

In Part Model :

/**
 * Get the materials for the part.
 *
 * @return BelongsToMany
 */
public function materials(): BelongsToMany
{
    return $this->belongsToMany(Material::class)
        ->using(MaterialPart::class);
}

With that config, it work pretty well when adding a row; when i create a Part, with 2 selected Materials, both Materials has 1 Part each, and the Part has 2 Materials. If I add another Part with the same materials the countcache work pretty well :

My Parts Capture d'écran 2023-12-07 224118 My Materials Capture d'écran 2023-12-07 224250

My Pivot Table Capture d'écran 2023-12-07 224301

The problem come when you delete a Part or a Material, the countcache isn't updated. Exemple, if I delete the Part test2, the Part count of the material BMP1 won't be updated and will stay at 2 instead of 1. I think there's something wrong about the observer when deleting (event not triggered ?)

EDIT : I found the solution (maybe not the most optimized, but it work)

I have setup an Observer (and registered them) for each Model :

<?php

namespace App\Observers;

use Illuminate\Support\Facades\Auth;
use App\Models\Part;

class PartObserver
{
    public function deleting(Part $part): void
    {
        $materials = $part->materials;

        foreach ($materials as $material) {
            $material->parts()->detach($part->getKey());
        }

    }
}

<?php

namespace App\Observers;

use Illuminate\Support\Facades\Auth;
use App\Models\Material;

class MaterialObserver
{
    public function deleting(Material $material): void
    {
        $parts = $material->parts;

        foreach ($parts as $part) {
            $part->materials()->detach($material->getKey());
        }

    }
}

So each time you delete a Part, the event deleting will be trrigered and for each material that was assigned to the Part, you detach them from the Part and it will trigger the countcache. Of course it work also when you delete a Material. This is insane wtf.

kirkbushell commented 5 months ago

@Xety I've just released v11 - did you want to test and see if it will work "out of the box"? V11 works directly with relationship objects, making it much easier to do aggregate counts.

kirkbushell commented 1 month ago

@Xety - see comment above.

Xety commented 1 month ago

@Xety - see comment above.

Hey, sorry I was very busy in the last couple of weeks. I will try it soon.

kirkbushell commented 1 month ago

@Xety - see comment above.

Hey, sorry I was very busy in the last couple of weeks. I will try it soon.

All good - did you get a chance?

Xety commented 2 weeks ago

@kirkbushell Hey, sorry for the late reply, I was busy IRL and needed to upgrade to Laravel11 too to test your package, so it required some work.

Well, I have tested it and it seems like I found an alternative to the old system. The only thing i changed was in the Pivot model.

In Pivot MaterialPart.php

<?php

namespace BDS\Models;

use Eloquence\Behaviours\CountCache\CountedBy;
use Eloquence\Behaviours\CountCache\HasCounts;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;

class MaterialPart extends Pivot
{
    use HasCounts;

    // BEFORE
    /**
     * Return the count cache configuration.
     *
     * @return array
     */
    /*public function countCaches(): array
    {
        return [
            [
                'model'      => Material::class,
                'field'      => 'part_count',
                'foreignKey' => 'material_id',
                'key'        => 'id'
            ],
            [
                'model'      => Part::class,
                'field'      => 'material_count',
                'foreignKey' => 'part_id',
                'key'        => 'id'
            ]
        ];
    }*/

    // AFTER
    #[CountedBy(as: 'part_count')]
    public function material(): BelongsTo
    {
        return $this->belongsTo(Material::class);
    }

    #[CountedBy(as: 'material_count')]
    public function part(): BelongsTo
    {
        return $this->belongsTo(Part::class);
    }
}

In model Part.php

public function materials(): BelongsToMany
{
    return $this->belongsToMany(Material::class)
        ->using(MaterialPart::class)
        ->withTimestamps();
}

In model Material.php

public function parts(): BelongsToMany
{
    return $this->belongsToMany(Part::class)
        ->using(MaterialPart::class);
}

I still have my observers too :

MaterialObserver.php

<?php

namespace BDS\Observers;

use BDS\Models\Material;

class MaterialObserver
{
    /**
     * Handle the "deleting" event.
     */
    public function deleting(Material $material): void
    {
        $parts = $material->parts;

        foreach ($parts as $part) {
            $part->materials()->detach($material->getKey());
        }
    }
}

MaterialObserver.php

<?php

namespace BDS\Observers;

use BDS\Models\Part;

class PartObserver
{
    /**
     * Handle the "deleting" event.
     */
    public function deleting(Part $part): void
    {
        $materials = $part->materials;

        foreach ($materials as $material) {
            $material->parts()->detach($part->getKey());
        }
    }
}

Looks like everythings work well again, if you can make some more tests to confirm it.

kirkbushell commented 1 week ago

@Xety Sorry, I require clarification - are you saying the count cache is working even with BelongsToMany relationships? Are changes required as part of Eloquence?

Xety commented 1 week ago

@kirkbushell With this setup, yes count cache is working with BelongsToMany relationships. No change required if you setup a Pivot model with 2 belongsTo relationships.

kirkbushell commented 1 week ago

@kirkbushell With this setup, yes count cache is working with BelongsToMany relationships. No change required if you setup a Pivot model with 2 belongsTo relationships.

Ah, right okay - so yeah some work needs to be done on Eloquence to support that. Thanks :)

Xety commented 1 week ago

@kirkbushell Well yeah, the best way would be to have something like that without Pivot model :

#[CountedBy(as: 'part_count')]
public function materials(): BelongsToMany
{
    return $this->belongsToMany(Material::class);
}

#[CountedBy(as: 'material_count')]
public function parts(): BelongsToMany
{
    return $this->belongsToMany(Part::class);
}

But at least i have something working until the official support.