Skip to content

Conversation

@roxblnfk
Copy link
Member

@roxblnfk roxblnfk commented Dec 10, 2025

🔍 What was changed

Added relations bulk loader to load relations

📝 Checklist

📃 Documentation: Bulk Loading Relations

When you fetch multiple entities and need to load their relations, doing it one by one causes the N+1 query problem.
BulkLoader solves this by loading relations for all entities at once.
Only unloaded relations are resolved - already loaded relations won't be overwritten.

Getting BulkLoader Instance

To load relations, use the \Cycle\ORM\Relation\BulkLoaderInterface.

If you're using a Cycle ORM bridge, get the BulkLoader instance from the DI container:

use Cycle\ORM\Relation\BulkLoaderInterface;

final class Service
{
    public function __construct(
        private BulkLoaderInterface $bulkLoader
    ) {}
}

If you're not using a bridge, configure the binding in your application container:

// Container configuration
$container->bindSingleton(
    \Cycle\ORM\Relation\BulkLoaderInterface::class,
    \Cycle\ORM\Relation\BulkLoader::class
);

Then inject it into your services as shown above.

Basic Usage

The BulkLoaderInterface::collect() method is immutable and returns a new instance
of Cycle\ORM\Relation\RelationLoaderInterface that you can use to chain load() calls and finally execute run().

$users = $userRepository->findAll();

$bulkLoader
    ->collect(...$users)
    ->load('profile')
    ->run();

foreach ($users as $user) {
    echo $user->profile->imageUrl; // No extra queries
}

This executes just 2 queries instead of 1 + N: one to fetch users, one to fetch all their profiles.

Multiple and Nested Relations

// Multiple relations
$bulkLoader
    ->collect(...$users)
    ->load('profile')
    ->load('comments')
    ->load('posts')
    ->run();

// Nested relations
$bulkLoader
    ->collect(...$users)
    ->load('posts.comments')
    ->run();

// With options
$bulkLoader
    ->collect(...$users)
    ->load('comments', [
        'orderBy' => ['created_at' => 'DESC'],
        'where' => ['approved' => true]
    ])
    ->run();

// Sort by pivot columns (many-to-many)
$bulkLoader
    ->collect(...$users)
    ->load('tags', ['orderBy' => ['@.@.created_at' => 'DESC']])
    ->run();

Important Behavior

  • All entities must be of the same role.

  • Entities must be loaded from the database (not new).

    $user = new User(); // not persisted or fetched
    
    $bulkLoader
        ->collect($user)
        ->load('profile')
        ->run(); // LogicException
  • Relations use database state, not runtime changes. It's required because to ensure consistency.

    $profile = $profileRepository->findByPK(1); // user_id = 5 in DB
    $profile->user_id = 10; // changed at runtime
    
    $bulkLoader->collect($profile)->load('user')->run();
    
    // $profile->user still points to User(5), not User(10)
  • Already loaded relations are not overwritten:

    $user = $userRepository->findByPK(1);
    $user->profile; // lazy loading triggered
    
    $bulkLoader->collect($user)->load('profile')->run();
    
    // $user->profile is the same instance, not reloaded

@codecov
Copy link

codecov bot commented Dec 10, 2025

Codecov Report

❌ Patch coverage is 94.68085% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.42%. Comparing base (42803a1) to head (bb6cbb5).
⚠️ Report is 14 commits behind head on 2.x.

Files with missing lines Patch % Lines
src/Parser/StaticNode.php 83.33% 2 Missing ⚠️
src/Select/UpdateLoader.php 83.33% 2 Missing ⚠️
src/Relation/BulkLoader.php 98.36% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                2.x     #539      +/-   ##
============================================
+ Coverage     89.93%   91.42%   +1.49%     
- Complexity     1892     1925      +33     
============================================
  Files           120      123       +3     
  Lines          4860     4947      +87     
============================================
+ Hits           4371     4523     +152     
+ Misses          489      424      -65     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roxblnfk roxblnfk merged commit 8ac5a6e into 2.x Dec 12, 2025
34 of 35 checks passed
@roxblnfk roxblnfk deleted the bulk-loader branch December 12, 2025 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💡 Efficient bulk-loading of morph relations

2 participants