Laravel & MongoDB – relationships with ObjectId

I’ve used Laravel and jenssegers/laravel-mongodb package for a long time. It’s wonderful thing, because it allows to use MongoDB easily, integrate most of things with Eloquent and Laravel Models, but also offers hybrid relationships with different database drivers. In theory it supports all relationships in MongoDB database, but in practise, there are a lot of potential issues because of current implementation. 

On MonogDB you can use many fields to create relations and use them in $lookups (aggregations) after that, but the most common practise is to use ObjectId – it’s default field type for keys (_id) and of course you can use different, but probably you will use that one. The problem is jenssegers package uses string for all relationships – and it works (but not always), until you want to use custom aggregations and lookups. These is an example of our parent model with some child relationship:

use Jenssegers\Mongodb\Relations\HasMany;

class ParentModel 
{
    public function children(): HasMany
    {
        return $this->hasMany(ChildModel::class, 'parent_model_id');
    }

}

And then you want to add some children to parent model instance:

$child = new ChildModel();
$parentModel->children()->save($child);

What will be the effect? Child record will have parent_model_id field of course, but it will be string field, not ObjectId. It’s fine until you will need to use $lookup – in that case, simple join will NOT work, because you have ObjectId key (_id) in Parent collection and string relation field on Child collection. We will need to prepare additional field using $addFields in Parrent collection before $lookup stage or in Child collection during $lookup pipeline. It’s not efficient way to solve that problem.

So, how to handle with that? Solution is easy and already available on new package development version: skip automatic casting key to string. Unfortunately, looks like it is not maintained actively, and we will wait a bit longer for updated version. But we can add required change right now. Just overwrite ParentModel getIdAttribute to fix that and always return clean value, without any modification:

class ParentModel
{
    public function getIdAttribute($value = null)
    {
        return $value;
    }
}

After that change code to add child:

$parentModel->children()->save($child);

will not use string in parent_model_id field anymore. It will be ObjectId and everything will work correctly: build-in Laravel relationships and also $lookups, without any additional fields.

Any drawbacks? Yes, after such change, you need to remember to cast key if you want to send it to client as a string or use in comparisons:

$parentModel->getKey() === (string)$parentModel->getKey() 
// before change: true
// after change: false, because left is ObjectId instance

// JSON resource:

return [
    'id' => (string) $parentModel->getKey(),

    // or
    'id' => $parentModel->getKey()->toString(),
]

But I think it’s not a big deal – it’s better to always have one type instead of many different in the same field.