skip to Main Content

Let’s say I have a model called Research. Each research belongsToMany Location models. And each Location model BelongsToMany Contact models. BUT, each Contact is also related to Research.

class Research extends Model {
   protected $table = 'researches';

   public function locations()
   {
      return BelongsToMany( Location::class, 'research_locations_list', 'research_id', 'location_id' );
   }
}

class Location extends Model {
   protected $table = 'locations';

   public function researches()
   {
      return BelongsToMany( Research::class, 'research_locations_list', 'research_id', 'location_id' );
   }

   public function contacts()
   {
      return BelongsToMany( Contact::class, 'location_contacts_list', 'location_id', 'contact_id' );
   }
}

class Contact extends Model {
   protected $table = 'contacts';

   public function locations()
   {
      return BelongsToMany( Location::class, 'location_contacts_list', 'location_id', 'contact_id' );
   }
}

researches table:

+----+------------+
| id |  research  |
+----+------------+
|  1 | Research 1 |
|  2 | Research 2 |
+----+------------+

locations table:

+----+---------------+
| id |   location    |
+----+---------------+
|  1 | United States |
|  2 | Great Britain |
|  3 | Germany       |
+----+---------------+

contacts table:

+----+---------+
| id | contact |
+----+---------+
|  1 | Jack    |
|  2 | John    |
|  3 | Hanz    |
+----+---------+

research_locations_list table:

+----+-------------+-------------+
| id | research_id | location_id |
+----+-------------+-------------+
|  1 |           1 |           1 |
|  2 |           1 |           2 |
|  3 |           2 |           2 |
|  4 |           2 |           3 |
+----+-------------+-------------+

So Research 1 is being conducted in United States and Great Britain, Research 2 in Great Britain and Germany

location_contacts_list table:

+----+-------------+------------+-------------+
| id | location_id | contact_id | research_id |
+----+-------------+------------+-------------+
|  1 |           1 |          1 |           1 |
|  2 |           1 |          2 |           1 |
|  3 |           2 |          1 |           2 |
|  4 |           3 |          3 |           2 |
+----+-------------+------------+-------------+

Research 1 should have Jack and John as contacts in United States and no contacts elsewhere;

Research 2 should have John as contact in Great Britain and Hanz in Germany;

Now, with lazy load I can achieve that:

$researches = Research::all();

foreach( $researches as $research )
{
    foreach( $research->locations as $location )
    {
        $contacts = $location->contacts()->wherePivot( 'research_id', $research->id )->get();
        // Will return John and Jack in United States for Research 1 and John in Great Britain and Hanz in Germany for Research 2
    }
}

Now, the question is: how do I achieve this with eager loading?

$researches = Research::with( 'locations.contacts' )->all();

foreach( $researches as $research )
{
    foreach( $research->locations as $location )
    {
        $contacts = $location->contacts;
        // Will return John and Jack in United States, John in Great Britain ( which is not supposed to happen ) for Research 1 and John in Great Britain and Hanz in Germany for Research 2
    }
}

Perhaps I can instruct somehow for contacts to respect ancestor id? Like:

$research = Research::with( 'locations.contacts' )->where( 'researches.id = location_contacts_list.research_id' )->all();

UPDATE

The closest I came up to solving this is modifying the Location model like this:

class Location extends Model {
   protected $table = 'locations';

   public function researches()
   {
      return BelongsToMany( Research::class, 'research_locations_list', 'research_id', 'location_id' );
   }

   public function contacts()
   {
      return BelongsToMany( Contact::class, 'location_contacts_list', 'location_id', 'contact_id' );
   }

   // Modify contacts attribute getter
   public function getContactsAttribute()
   {
      $contacts = $this->contacts();
      
      if( !empty( $this->pivot->research_id ) )
      {
         $contacts = $contacts->wherePivot( 'research_id', $this->pivot->research_id );
      }
      
      return $contacts->get();
   }
}

But it looks kind of dirty…

3

Answers


  1. If I got it right, you want to add some conditions inside your with statement. If you want to use eloquent syntax, you can do it like this:

    $research = Research::with(['YOUR RELATION' => function ($query) {
        $query->where('YOUR COLUMN', 'EQUALS TO SOMETHING');
    }])->get();
    

    Keep in mind that since inside with you use nested relationships, like locations.contacts, the where function inside the query, will filter only the last model (in this case that would be contacts). If you want to filter both locations and contacts based on some conditions, you have to write something similar to this (just an example):

    $research = Research::with(['locations' => function ($query) {
        $query->where('id', 1)->with(['contacts' => function ($query) {
            $query->where('name', 'Tim');
        }]);
    })->get();
    

    In order to do that though, you need to create a relationship also with your pivot table (if you want to use it also inside the conditions). Otherwise, you have to use a different syntax, using joins. Check this page from docs for query builders https://laravel.com/docs/9.x/queries#main-content

    Login or Signup to reply.
  2. In your solution you get N+1 query problem. I can suggest the following solution:

    class Research extends Model
    {
        protected $table = 'researches';
    
        public function locations(): BelongsToMany
        {
            return $this->belongsToMany(Location::class, 'research_locations_list');
        }
    
        public function contacts(): BelongsToMany
        {
            return $this->belongsToMany(Contact::class, 'location_contacts_list')
                ->withPivot('location_id');
        }
    
        public function contactsByLocationAttribute(int $locationId): Collection
        {
            return $this->contacts
                ->filter(static function ($contact) use ($locationId) {
                    return $contact->pivot->location_id === $locationId;
                });
        }
    }
    
    
    $researches = Research::with(['locations', 'contacts'])->get();
    foreach ($researches as $research) {
        foreach ($research->locations as $location) {
            $contacts = $research->contactsByLocation($location->id);
        }
    }
    

    here there will always be only 3 queries to the database. And only necessary models will be loaded

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search