Repository: livewire/livewire (php)

Issue:
Eager Loading Relationships
I'm not sure if this is a bug or just a missing feature or maybe I'm just doing something wrong. The idea is that I want to eager load a relationship to resolve the n + 1 problem.

In the mount method I have a query where I load the relationship with the "with" method. If I make a "dd", the relationship is there and loaded:
`$query = OfferFile::select();

$query->where('offer_id', '=', $this->offer_id);

$query->with('File');

this->offerFiles = $query->get();`

![image](https://user-images.githubusercontent.com/43000085/98798685-8c66e000-23e4-11eb-8aec-9608f01302d4.png)

But then in the Laravel Debug bar I still have all the queries running for each relationship one by one when it's rendered:
![image](https://user-images.githubusercontent.com/43000085/98799114-1151f980-23e5-11eb-90a2-db259957c209.png)

Then when I go back the Livewire Class for example with a button click, the relationship is missing. That means the relationship doesn't survive the trip to the blade?
![image](https://user-images.githubusercontent.com/43000085/98799215-35add600-23e5-11eb-9b62-4b3a5eaa82b9.png)

Is that a bug? Or just not supported by Livewire? Or how is the recommended way to solve the n + 1 problem?

**Context**
- Livewire version: 2.2.9
- Laravel version: 8.9.0
- Browser: Chrome


Pull request description:
[Updated - loadMissing] Restore collection relations when hydrating
UPDATE
Use `loadMissing`. 

Thanks to @DanielCoulbourne, that method is perfect for this!

Reference: https://youtu.be/NjUj4oGGCbQ?t=4401

---

This PR adds eloquent collection relation restoration to the hydration process to stop N+1 issues (see #2010).

Ideally this is a short-term fix with this eventually being fixed upstream in the Laravel framework so it's no longer needed here.

I have included a check to ensure that, if this does get fixed upstream, and collection relations are loaded in Laravel's `SerializesAndRestoresModelIdentifiers` class, that Livewire won't load them again. Otherwise this would cause a double up of database requests for the relations (turns out `->load()` isn't smart enough to know when a relation is already loaded).

I've added tests for both collection with eager loaded relations, and without, to ensure all is working as expected.

I couldn't add a test for the upstream relation loaded check as I have no way to hook in and preload the relations after the collection has been restored but before this call. But I have manually tested it with a local version of Laravel with the patch applied and ensured that the check works as expected and that there wasn't a double of up database requests.

Hope this helps!

Fixes #2010

Patch:
diff --git a/src/HydrationMiddleware/HydratePublicProperties.php b/src/HydrationMiddleware/HydratePublicProperties.php
index 634bd7a8..be7eeddc 100644
--- a/src/HydrationMiddleware/HydratePublicProperties.php
+++ b/src/HydrationMiddleware/HydratePublicProperties.php
@@ -120,6 +120,12 @@ class HydratePublicProperties implements HydrationMiddleware
             new ModelIdentifier($serialized['class'], $serialized['id'], $serialized['relations'], $serialized['connection'])
         );
 
+        /*
+         * Use `loadMissing` here incase loading collection relations gets fixed in Laravel framework,
+         * in which case we don't want to load relations again.
+         */
+        $models->loadMissing($serialized['relations']);
+
         $dirtyModelData = $request->memo['data'][$property];
 
         foreach ($idsWithNullsIntersparsed as $index => $id) {


Test patch:
diff --git a/tests/Browser/DataBinding/EagerLoading/Component.php b/tests/Browser/DataBinding/EagerLoading/Component.php
new file mode 100644
index 00000000..d640b360
--- /dev/null
+++ b/tests/Browser/DataBinding/EagerLoading/Component.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Tests\Browser\DataBinding\EagerLoading;
+
+use Livewire\Component as BaseComponent;
+
+class Component extends BaseComponent
+{
+    public $posts;
+
+    public $comments;
+
+    public function mount()
+    {
+        $this->posts = Post::with('comments')->get();
+        $this->comments = Comment::all();
+    }
+
+    public function postsCommentsRelationIsLoaded()
+    {
+        return $this->posts->every(function ($post) {
+            return $post->relationLoaded('comments');
+        });
+    }
+
+    public function commentsHaveNoRelations()
+    {
+        return $this->comments->every(function ($comments) {
+            return $comments->getRelations() === [];
+        });
+    }
+
+    public function render()
+    {
+        return <<<'HTML'
+<div>
+    <div dusk="posts-comments-relation-loaded">
+        {{ $this->postsCommentsRelationIsLoaded() ? 'true' : 'false' }}
+    </div>
+
+    <div dusk="comments-has-no-relations">
+        {{ $this->commentsHaveNoRelations() ? 'true' : 'false' }}
+    </div>
+
+    <button dusk="refresh-server" type="button" wire:click="$refresh">Refresh Server</button>
+</div>
+HTML;
+    }
+}
diff --git a/tests/Browser/DataBinding/EagerLoading/Test.php b/tests/Browser/DataBinding/EagerLoading/Test.php
new file mode 100644
index 00000000..5b1e9090
--- /dev/null
+++ b/tests/Browser/DataBinding/EagerLoading/Test.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Tests\Browser\DataBinding\EagerLoading;
+
+use Illuminate\Database\Eloquent\Model;
+use Laravel\Dusk\Browser;
+use Livewire\Livewire;
+use Sushi\Sushi;
+use Tests\Browser\TestCase;
+
+class Test extends TestCase
+{
+    /** @test */
+    public function it_restores_eloquent_colletion_eager_loaded_relations_on_hydrate()
+    {
+        $this->browse(function (Browser $browser) {
+            Livewire::visit($browser, Component::class)
+                    ->assertSeeIn('@posts-comments-relation-loaded', 'true')
+                    ->waitForLivewire()->click('@refresh-server')
+                    ->assertSeeIn('@posts-comments-relation-loaded', 'true')
+                    ;
+        });
+    }
+
+    /** @test */
+    public function models_without_eager_loaded_relations_are_not_affected()
+    {
+        $this->browse(function (Browser $browser) {
+            Livewire::visit($browser, Component::class)
+                    ->assertSeeIn('@comments-has-no-relations', 'true')
+                    ->waitForLivewire()->click('@refresh-server')
+                    ->assertSeeIn('@comments-has-no-relations', 'true')
+                    ;
+        });
+    }
+}
+
+class Post extends Model
+{
+    use Sushi;
+
+    protected $rows = [
+        ['id' => 1, 'name' => 'post1'],
+        ['id' => 2, 'name' => 'post2'],
+    ];
+
+    public function comments()
+    {
+        return $this->hasMany(Comment::class);
+    }
+}
+
+class Comment extends Model
+{
+    use Sushi;
+
+    protected $rows = [
+        ['comment' => 'comment1', 'post_id' => 1],
+        ['comment' => 'comment2', 'post_id' => 1],
+        ['comment' => 'comment3', 'post_id' => 1],
+        ['comment' => 'comment4', 'post_id' => 1],
+        ['comment' => 'comment5', 'post_id' => 2],
+        ['comment' => 'comment6', 'post_id' => 2],
+    ];
+
+    public function post()
+    {
+        return $this->belongsTo(Post::class);
+    }
+}