mongodb / laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel (Moloquent)
https://www.mongodb.com/compatibility/mongodb-laravel-integration
MIT License
7k stars 1.42k forks source link

whereHas does not work with ObjectId relations #2986

Open zliebersbach opened 4 months ago

zliebersbach commented 4 months ago

Description:

Cannot use whereHas on relations that use ObjectId. ObjectId is the best-practice method for storing related document IDs.

Loading the relation works as expected, but not whereHas.

If this is possible, I propose adding the solution to the documentation, otherwise a fix should be introduced.

Steps to reproduce

  1. EntityB has the following relation configured:

    protected $casts = [
        'entity_a_id' => ObjectId::class,
    ];
    
    public function entityA(): BelongsTo
    {
        return $this->belongsTo(EntityA::class);
    }
  2. EntityB::whereHas('entityA', fn ($query) => $query->where('status', Status::ADDED))->get()

Expected behaviour

The following query should be executed (collected from DB::getQueryLog()):

"entity_a.find({"status":"A"},{"projection":{"_id":true},"typeMap":{"root":"array","document":"array"}})"
"entity_b.find({"entity_a_id":{"$in":[ObjectId("66591310b42a652a7c027862"),ObjectId("66591310b42a652a7c027863")]}},{"typeMap":{"root":"array","document":"array"}})"

Actual behaviour

The following query is executed (collected from DB::getQueryLog()):

"entity_a.find({"status":"A"},{"projection":{"_id":true},"typeMap":{"root":"array","document":"array"}})"
"entity_b.find({"entity_a_id":{"$in":["66591310b42a652a7c027862","66591310b42a652a7c027863"]}},{"typeMap":{"root":"array","document":"array"}})"
Logs:
julianbarrerar commented 3 months ago

@zliebersbach Encontré una posible solución mientras se soluciona, no se si es la mejor forma:

Creas un trait en app/Traits/EloquentWhereHas.php

<?php

namespace App\Traits;
use MongoDB\BSON\ObjectId;

use Illuminate\Http\Response;

trait EloquentWhereHas{

    public function scopeWhereHasTemp($query, $relation, $cb)
{
    $relationInstance = $this->$relation();
    $relatedModelQuery = $relationInstance->getRelated()->newQuery();
    call_user_func($cb, $relatedModelQuery);
    if ($relationInstance instanceof \Illuminate\Database\Eloquent\Relations\BelongsTo) {
        $foreignKey = $relationInstance->getForeignKeyName();
        $parentKey = $relationInstance->getOwnerKeyName();
    } else {
        $foreignKey = $relationInstance->getLocalKeyName();
        $parentKey = $relationInstance->getForeignKeyName();
    }
    if($foreignKey == 'id') $foreignKey = "_id";
    $objectIds = $relatedModelQuery->pluck($parentKey)->toArray();
    // Agregar si es necesario
    /* $objectIds = array_map(function ($id) {
        return new ObjectId($id);
    }, $objectIds); */
    $query->whereIn($foreignKey, $objectIds);
    return $query;
}

}

Usas el trait en el modelo dentro de la clase:

use App\Traits\EloquentWhereHas;

class MiModelo extends Model {
        use EloquentWhereHas;
        public $collection = "mimodelo";
        ....

En tu consulta MiModelo::whereHasTemp('nameRelation', function($query) { $query->where('status', true);})->get() En conclusión

Revisando el Log, la diferencia es que cambia el "id" por "_id", ejemplo log:

con whereHas

{"id":{"$in":["641a45c05908a72267057c3f"]}}

con el trait "whereHasTemp"

{"_id":{"$in":["641a45c05908a72267057c3f"]}}