Skip to content

Commit 092d018

Browse files
authored
Merge pull request #61 from webrium/fix/eager-loading-with-after-select-and-orderby
test(relations): add explicit N+1 query-count assertions
2 parents a6e625d + 9a8b851 commit 092d018

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

tests/Integration/RelationsTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,114 @@ public function test_select_without_with_is_unaffected(): void
303303
$this->assertInstanceOf(RelUser::class, $user);
304304
$this->assertSame('Alice', $user->name);
305305
}
306+
307+
// -----------------------------------------------------------------------
308+
// N+1 verification — proves eager loading runs a constant number of
309+
// queries (2: one for parents, one whereIn() for the relation) no
310+
// matter how many parent rows are involved, instead of one extra
311+
// query per row (the classic N+1 problem).
312+
// -----------------------------------------------------------------------
313+
314+
/**
315+
* Seed many users (more than the small fixture in seedAll()) so a
316+
* per-row query difference is unambiguous and not a fluke of a
317+
* tiny dataset.
318+
*
319+
* @return int Number of users seeded
320+
*/
321+
private function seedManyUsersWithPosts(int $userCount = 10, int $postsPerUser = 3): int
322+
{
323+
for ($i = 1; $i <= $userCount; $i++) {
324+
$user = RelUser::create(['name' => "Bulk User {$i}"]);
325+
for ($j = 1; $j <= $postsPerUser; $j++) {
326+
RelPost::create(['user_id' => $user->getKey(), 'title' => "Bulk Post {$j}"]);
327+
}
328+
}
329+
330+
return $userCount;
331+
}
332+
333+
public function test_lazy_loading_issues_one_query_per_row(): void
334+
{
335+
$userCount = $this->seedManyUsersWithPosts(userCount: 10);
336+
337+
DB::flushQueryLog();
338+
DB::enableQueryLog();
339+
340+
$users = RelUser::all();
341+
foreach ($users as $user) {
342+
$user->posts->count(); // lazy: one query per iteration
343+
}
344+
345+
$queryCount = count(DB::getQueryLog());
346+
347+
// 1 query to fetch the users + 1 query per user for posts.
348+
$this->assertSame(
349+
1 + $userCount,
350+
$queryCount,
351+
'Lazy loading should issue exactly one query per parent row (the N+1 pattern), confirming the baseline this test contrasts eager loading against.',
352+
);
353+
}
354+
355+
public function test_eager_loading_issues_constant_query_count_regardless_of_row_count(): void
356+
{
357+
$userCount = $this->seedManyUsersWithPosts(userCount: 10);
358+
359+
DB::flushQueryLog();
360+
DB::enableQueryLog();
361+
362+
$users = RelUser::with('posts')->get();
363+
foreach ($users as $user) {
364+
$user->posts->count(); // already eager-loaded — must not query again
365+
}
366+
367+
$queryCount = count(DB::getQueryLog());
368+
369+
// Exactly 2 queries total: 1 for users, 1 whereIn(...) for all posts —
370+
// independent of how many users were fetched. This is the N+1 fix.
371+
$this->assertSame(
372+
2,
373+
$queryCount,
374+
'Eager loading via with() must use a constant 2 queries (parents + one whereIn for the relation), not one query per row.',
375+
);
376+
377+
$log = DB::getQueryLog();
378+
$this->assertStringContainsString('IN (', $log[1]->sql, 'The eager relation query should batch all parent keys into a single whereIn(...) clause.');
379+
}
380+
381+
public function test_eager_loading_after_select_also_avoids_n_plus_1(): void
382+
{
383+
// Same as above, but through the select()->with() chain that was
384+
// previously broken — confirms the fix didn't just make the call
385+
// succeed, but that it still avoids N+1 once it does.
386+
$userCount = $this->seedManyUsersWithPosts(userCount: 10);
387+
388+
DB::flushQueryLog();
389+
DB::enableQueryLog();
390+
391+
$users = RelUser::select('id', 'name')->with('posts')->get();
392+
foreach ($users as $user) {
393+
$user->posts->count();
394+
}
395+
396+
$this->assertSame(2, count(DB::getQueryLog()));
397+
}
398+
399+
public function test_eager_loading_belongs_to_avoids_n_plus_1(): void
400+
{
401+
// Inverse direction: many posts eager-loading their single author.
402+
$this->seedManyUsersWithPosts(userCount: 10, postsPerUser: 2);
403+
404+
DB::flushQueryLog();
405+
DB::enableQueryLog();
406+
407+
$posts = RelPost::with('author')->get();
408+
foreach ($posts as $post) {
409+
$post->author->name;
410+
}
411+
412+
$this->assertSame(2, count(DB::getQueryLog()));
413+
}
306414
}
307415

308416
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)