Laravel Eloquent: MongoDB relations

Laravel offers very complex Eloquent – ORM simple to use, but with a lot of options. One of them is relations support. We can define several types of relations between our models and then use them to simplify get the data. Relations are also support by MongoDB extension for Eloquent, which is available on GitHub. Sometimes it may be difficult to use because of some issues with ObjectId and BSON. Let’s see how it can be solved.

If you know Eloquent relations, you know what is for example hasOne method and how can we use it. On standard models it will work like on very simple examples:

public function category()
{
    return $this->hasOne(Category::class,  'id', 'category_id');
}

You can want to use this or other relations in similar way on MongoDB. It’s and example objects in such database:

// Categories
{
	_id: ObjectId('5c8445700000000000000000'),
	name: 'Foo',
	description: 'Test foo'
}

// Posts
{
	_id: ObjectId('5c8445700000000000000001'),
	name: 'Bar',
	text: 'Test foo',
	category_id: ObjectId('5c8445700000000000000000')
}

And some code to define relation:

public function category()
{
    return $this->hasOne(CategoryMongo::class,  '_id', 'category_id');
}

Everything is right, yes? Unfortunately, not exactly. It will not work as we expect, because queries will throw an error about “cast string “. It’s because we store data as ObjectId and inside internal methods, Eloquent use them in $in operator in queries. You can find some solutions – to use .toString() in relation method in local key:

public function category()
{
    return $this->hasOne(CategoryMongo::class,  '_id.toString()', 'category_id.toString()');
}

But also, it will not work as expected – you will find a lot first record from database in this field, not real relation, but first record from related collection. Strange? Yes. So, how we can solve this? First method is to use string to save local relations to foreign collections instead of using ObjectId. It will work fine then. I can’t recommend that for two reasons: first, we don’t always have ability to convert exciting fields to new format. Second, it isn’t compatible with all MongoDB functions – if you will decide to use raw queries, aggregate with $lookup phase, it will be much harder to implement. We should always use MongoDB native types like ObjectId for keys and UTCDateTime for dates.

Solution is simple – in Laravel with Eloquent of course – we have to specify casting in our models and cast local ObjectId keys into string. After that, “magic” inside Laravel will automatically change keys to strings and then use them to get relations. Everything will be fine, and will will be able to still use advanced raw MongoDB queries. We must only add protected property called $casts to our model, it will provide all required functions:

// Post model
protected $casts = [
    'category_id' => 'string',
];

And it’s all. After that change, relations in MongoDB using Eloquent will works as expected – and you don’t have to use any modifications like .toString(). Of course be “aware” – there is lazy load like on standard Eloquent relations, you can alo use “with” in queries, but on MongoDB, it will NOT work like MySQL JOIN! It runs additional queries to get data. If you want to get all data with only one query, you should use $aggregation with $lookup in MongoDB.