skip to Main Content

As @JonasStaudenmeir answered on laravel eager loading with limit, which query looks like:

User::select('id')
    ->with([
        'posts' => fn($query) => $query->select(['id', 'user_id'])->limit(4)
    ])
    ->limit(2)
->get();

enter image description here

select `id` from `users` limit 2

select * from (select `id`, `user_id`, row_number() over (partition by `posts`.`user_id`) as laravel_row from `posts` where `posts`.`user_id` in (1, 3)) as laravel_table where laravel_row <= 4 order by laravel_row

Now, my interest is to do it manually which is what I tried here:

User::select('id')
->with([
    'posts' => fn($query) => $query->select(['id', 'user_id'])
        ->selectRaw("row_number() over (partition by `posts`.`user_id`) as laravel_row")
        ->where('laravel_row', '<=', 4)
        ->orderBy('laravel_row')
])
->limit(2)
->get();

Also, I take some help from online (SQLtoEloquent), but the syntax was not properly formed so it failed there too.

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'laravel_row' in 'where clause'

select `id`, `user_id`, row_number() over (partition by `posts`.`user_id`) as laravel_row from `posts` where `posts`.`user_id` in (1, 3) and `laravel_row` <= 4 order by `laravel_row` asc

UPDATE:

Following @Igor’s advice, I tried to make it similar to the package and more convenient but didn’t match the expected output.

AppProvidersAppServiceProvider.php

public function boot(): void
{
    IlluminateDatabaseEloquentRelationsRelation::macro('limit', function(int $value) {

        if($this->parent->exists) {

           $this->query->limit($value);

        } elseif ($value >= 0) {

            // When I tried on the Model(User), it succeeded the below logic
            // 2 was expected because the total number of users fetched is 2 but it didn't happen
            $parentLimitValue = $this->query->getQuery()->limit; // null

            // $parentLimitValue = $this->parent->getQuery()->limit; // null
            // $parentLimitValue = $this->related->getQuery()->limit; // null

            $parentLimitValue ??= 1;

            $this->query
                ->selectRaw("row_number() over (partition by ".$this->getExistenceCompareKey().") as laravel_row")
                ->orderBy('laravel_row')
            ->limit($value * ($parentLimitValue ?: 1));
        }
        return $this;
    });
}

Does anyone know where I should put my eyes to minimize this package?

2

Answers


  1. Chosen as BEST ANSWER

    As per @Igor's response, I made it more handy by placing it in traits & local scopes.

    AppTraitsWithEagerLimit.php

    <?php
    
    namespace AppTraits;
    
    use IlluminateDatabaseEloquentBuilder;
    
    trait WithEagerLimit
    {
        public function scopeWithEagerLimit(Builder $builder, string $relation, callable $callback)
        {
            return $builder->with([ $relation => function($query) use ($builder, $callback) {
    
                $limit = $builder->getQuery()->limit ?: 1;
    
                // IlluminateDatabaseEloquentRelationsRelation
                $query = call_user_func_array($callback, [$query]);
    
                // IlluminateDatabaseQueryBuilder
                $dbQB = $query->getQuery()->getQuery();
    
                if(!$dbQB->limit) {
                    return $query;
                }
    
                $dbQB->limit *=  $limit;
    
                return $query->when(is_null($dbQB->columns), fn($q) => $q->select('*'))
                    ->selectRaw("row_number() over (partition by ".$query->getExistenceCompareKey().") as laravel_row")
                    ->orderBy('laravel_row');
            }]);
        }
    }
    

    Use the WithEagerLimit trait on the corresponding model.

    For example:

    class User extends Model {
    
        use AppTraitsWithEagerLimit;
        
        //...
    
        public function posts()
        {
            return $this->hasMany(Post::class);
        }
    }
    

    Now, you can chain withEagerLimit() to any of your queries by doing the following:

    Simple eager loading functionality

    User::withEagerLimit('posts', fn($query) => $query)->get();
    
    // SQL
    select * from `users`
    
    select * from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    

    Conditions on eager loading functionality

    User::query()
        ->withEagerLimit('posts', fn($query) => $query->select(['id', 'user_id'])->where('id', '<=', 5))
    ->get();
    
    // SQL
    select * from `users`
    
    select `id`, `user_id` from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) and `id` <= 5
    

    Complex SQL RAW query on eager loading functionality

    User::query()
        ->select('id')
        ->withEagerLimit('posts',
            fn($query) => $query->select(['id', 'user_id'])
                ->whereRaw('MOD(id, 2) = 0')
                ->limit(4)
        )
        ->whereRaw('MOD(id, 2) = 1')
        ->skip(3)
        ->take(2)
        ->latest('id')
    ->get();
    
    // SQL
    select `id` from `users` where MOD(id, 2) = 1 order by `id` desc limit 2 offset 3
    
    select `id`, `user_id`, row_number() over (partition by posts.user_id) as laravel_row from `posts` where `posts`.`user_id` in (1, 3) and MOD(id, 2) = 0 order by `laravel_row` asc limit 8
    

    Pagination on eager loading functionality

    User::query()
        ->withEagerLimit('posts', fn($query) => $query->limit(4))
    ->paginate(5);
    
    // SQL
    select count(*) as aggregate from `users`
    
    select * from `users` limit 5 offset 0
    
    select *, row_number() over (partition by posts.user_id) as laravel_row from `posts` where `posts`.`user_id` in (1, 2, 3, 4, 5) order by `laravel_row` asc limit 20
    

    For those eagerly waiting for an answer

    You should call ->limit($value) or an equivalent method (i.e. take(), skip(), paginate(), or similar procedure) in any queries chained with ->withEagerLimit($relationName, $callback) in the main-query and also in sub-query.

    User::query()
        ->select('id')
        ->withEagerLimit('posts', fn($query) => $query->select(['id', 'user_id'])->limit(4))
        ->limit(2)
    ->get();
    

  2. I think you need to remove where and add limit into the subquery

    User::select('id')
    ->with([
        'posts' => fn($query) => $query->select(['id', 'user_id'])
            ->selectRaw("row_number() over (partition by `posts`.`user_id`) as laravel_row")
            ->orderBy('laravel_row')
            ->limit(4) 
    ])
    ->limit(2)
    ->get();
    

    UPDATE

    To limit users and posts per users use this:

    $usersCount = 2;
    $postsPerUser = 4;
    $users = User::select('id')
    ->with([
        'posts' => fn($query) => $query->select(['id', 'user_id'])
            ->selectRaw("row_number() over (partition by `posts`.`user_id`) as laravel_row")
            ->limit($usersCount * $postsPerUser) 
            ->orderBy('laravel_row')
    ])
    ->limit($usersCount)
    ->get();
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search