Eager loading Eloquent properties

2017/10/24

Tags: laravel php

I’m currently working on a Laravel project which requires that each “resource” 1 be identified by a unique, non-sequential ID.

For a variety of reasons, these UUIDs are all stored in a separate uuids table, and associated with the resource via a polymorphic relationship.

Following Laravel’s conventions, we name the polymorphic relationship as clumsily as possible, by appending the suffix “able” to our property name. This gives us the following Uuid model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Uuid extends Model
{
    /**
     * Do not auto-increment IDs, as we're using UUIDs, natch.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * The mass-assignable attributes.
     *
     * @var array
     */
    protected $fillable = [
        'uuid',
        'uuidable_id',
        'uuidable_type',
    ];

    /**
     * Use the `uuid` column as our primary key.
     *
     * @var string
     */
    protected $primaryKey = 'uuid';

    /**
     * Returns the target of the polymorphic UUID relationship.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function uuidable()
    {
        return $this->morphTo();
    }
}

So far, so simple.

The dreaded N + 1

The primary downside to this arrangement is that each resource now requires an additional database query, just to retrieve its UUID. In other words, we just built ourselves an N + 1 problem.

The standard solution to this is to eager-load the relationship. Assuming we have a Project resource, here’s how that would look:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    protected $with = ['uuid'];
    
    public function uuid()
    {
        return $this->morphOne(Uuid::class, 'uuidable');
    }
}

The uuid method defines the relationship between the Project model and the Uuid model, and the with property tells Eloquent to eager-load this relationship.

So far, still so simple.

Unfortunately, this project has its fair share of legacy code, and for a variety of unspeakable reasons, the above code won’t cut it. So much for simple.

Eager loading Eloquent properties

Luckily for us, we’re really not that interested in the full Uuid model; all we care about is its uuid property.

This means we can load all of the UUIDs associated with a particular type of resource when that resource model first boots, and store the UUID strings in a static class property.

We’ll use an associative array, where the key is the model ID, and the value is the associated UUID.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    /**
     * An associative array of IDs to UUIDs.
     *
     * @var array
     */
    protected static $uuids;
    
    /**
     * Preload the UUIDs when the model boots.
     */ 
    protected static function boot()
    {
        parent::boot();
        static::preloadUuids();
    }
    
    /**
     * Preloads all of the UUIDs for this model type, and stores them in a
     * class property.
     */
    protected static function preloadUuids()
    {
        static::$uuids = Uuid::where('uuidable_type', static::class)
            ->pluck('uuid', 'uuidable_id')
            ->all();
    }
}

Now we just need to add a uuid custom attribute to the resource, which retrieves the instance’s UUID from the $uuids class property:

class Project extends Model
{
    // Existing code omitted for clarity.
    
    /**
     * Defines additional model attributes.
     *
     * @var array
     */
    protected $appends = ['uuid'];
    
    /**
     * Returns the UUID associated with this instance.
     *
     * @return string
     */
    public function getUuidAttribute(): string
    {
        return array_key_exists($this->id, static::$uuids)
            ? static::$uuids[$this->id]
            : '';
    }
}

By pre-populating the $uuids class property, we add a total of one additional query per resource type, regardless of how many times we use that resource type in the course of a single request.

Tidying up

Given that we’re using UUIDs with dozens of different resources, it makes sense to extract this code into a trait:

<?php

namespace App\Traits;

use App\Uuid;

trait HasUuid
{
    /**
     * An associative array of IDs to UUIDs.
     *
     * @var array
     */
    protected static $uuids;

    /**
     * Preloads the UUIDs for this model type.
     */
    protected static function bootHasUuid()
    {
        static::preloadUuids();
    }

    /**
     * Preloads all of the UUIDs for this model type, and stores them in a
     * class property.
     */
    protected static function preloadUuids()
    {
        static::$uuids = Uuid::where('uuidable_type', static::class)
            ->pluck('uuid', 'uuidable_id')
            ->all();
    }

    /**
     * Returns the UUID associated with this instance.
     *
     * @return string
     */
    public function getUuidAttribute(): string
    {
        return array_key_exists($this->id, static::$uuids)
            ? static::$uuids[$this->id]
            : '';
    }
}

Once that’s done, we can clean up our resource model.

<?php

namespace App;

use App\Traits\HasUuid;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    use HasUuid;
}

Conclusion

If you need to access the full related model, this probably isn’t the right choice. However, for situations where you’re really only interested in one or two specific values on the related model, it’s a nice, simple solution, which adds a maximum of one database query per model class.

For the project in question, this one change eliminated hundreds of queries from some of the most heavily-used pages in the application.


  1. In the context of this article, “resource” should be read as “Eloquent model instance”.