csheaff's avatar

Custom Eloquent BelongsTo

I have an Eloquent BelongsTo relation that works but is inefficient as it uses actual code. Basically, I have Districts and Schools that each could BelongTo another model foo. So $district->foo and $school->foo are valid, and since a School also belongs to a District, I can say $school->district->foo and that works. The issue is that foo might be null for a School, and I want it to 'inherit' the parent foo. So I wrote deFactoFoo() in my School class which looks like this:

public function deFactoFoo(): BelongsTo
{
    if (! (bool) $this->foo && (bool) $this->school)
        return $this->school->foo();
    }
    return $this->foo();
}

But this will incur extra DB queries when I write code like:

$bars = $user->school->deFactoFoo->bars;

and I can't eager load on deFactoFoo.

I looked into MariaDB virtual columns to transfer this logic to the DB layer, but it appears that those can only reference data from the current row. Is there a good way to integrate this kind of hierarchical or cascading Eloquent Relations into a Model class by crafting some custom class that implements BelongsTo and overloads getRelatedKey raw SQL?

My other thought was to replace the Model's base table with a view that includes what is effectively a virtual column.

Is one approach better than the other?

0 likes
5 replies
csheaff's avatar

PROGRESS UPDATE:

I can create views and use this views for the models which work great for SELECT and UPDATE queries, but fail for INSERT queries (see code at bottom).

At this point, I feel like my options are try to modify the Eloquent models to target the real, non-view table for INSERTs, or maybe there's a pre-INSERT hook where I can create a record behind the scenes and silently convert any School::create() or $school->save() that's an INSERT into an INSERT on the real table, followed by an UPDATE on the view. Hmmm, maybe I can override function save() and static function create() on these models to use the proper table...

Ideally there's a more seamless way in MariaDB to add a 'synthetic' read-only column like this such that Laravel treats it like a proper table more universally.

CREATE VIEW schools_with_de_facto_foo AS
SELECT 
    `schools`.*,
    IF(`schools`.foo_id, `schools`.foo_id, `districts`.foo_id) as de_facto_foo_id
FROM `schools` 
LEFT JOIN `districts` on `districts`.id = `schools`.district_id;

CREATE VIEW studios_with_de_facto_foo AS
SELECT 
    `studios`.*,
    IF(`studios`.foo_id, `studios`.foo_id, 
        IF(`schools`.foo_id, `schools`.foo_id, d.foo_id)
    )
    as de_facto_foo_id
FROM `studios`
LEFT JOIN `schools` on `schools`.id = `studios`.school_id
LEFT JOIN `districts` d on d.id = `schools`.district_id;

MariaDB documentation says:

A view cannot be used for inserting if it fails any of the criteria for updating, and must also meet the following conditions:

  • the view contains all base table columns that don't have default values
  • no base table columns are present in view select list more than once
  • the view columns are all simple columns, and not derived in any way. The following are examples of derived columns
    • column_name + 25
    • LOWER(column_name)
    • (subquery)
    • 9.5
    • column1 / column2
csheaff's avatar

Getting closer.

I added the views and then the following to the models:

School.php

I did have to wrestle a little bit with some foreign key stuff that's particular to my implementation, but that's pretty much settled. The only thing now is that the fresh() and refresh() functions will not hydrate deFactoFoo. It's not clear to me why.

Lumethys's avatar

sounds like you want to take a look at HasManyThrough/ HasOneThrough

csheaff's avatar

@Lumethys sorry if I wasn't clear in my posts, but this is not solved by HasManyThrough / HasOneThrough. This is a way to add some logic to a DB query to reduce the number of calls to the DB.

Please or to participate in this conversation.