Применить красноречивый запрос ограничения с использованием SQL ROW_NUMBER в Laravel

Как @JonasStaudenmeir ответил на интенсивную загрузку laravel с ограничением, этот запрос выглядит так:

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

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

Теперь мне интересно сделать это вручную, что я и пробовал здесь:

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();

Кроме того, я пользуюсь некоторой помощью в Интернете (SQLtoEloquent), но синтаксис был сформирован неправильно, поэтому и там он не работал.

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

ОБНОВЛЯТЬ:

Следуя совету @Igor, я попытался сделать его похожим на пакет и более удобным, но не соответствовал ожидаемому результату.

Приложение\Провайдеры\AppServiceProvider.php

public function boot(): void
{
    \Illuminate\Database\Eloquent\Relations\Relation::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;
    });
}

Кто-нибудь знает, куда я должен положить глаза, чтобы свернуть этот пакет?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
0
58
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, вам нужно удалить where и добавить ограничение в подзапрос

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();

ОБНОВЛЯТЬ

Чтобы ограничить пользователей и сообщения для пользователей, используйте это:

$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();

Ага! Но вопрос в том, как данные будут устроены/взяты. Вместо того, чтобы получать 4 сообщения для каждого пользователя, он будет получать 2-2 сообщения для каждого пользователя. Итак, это не то, что я ожидал.

JS TECH 23.07.2023 18:22

Попробуйте с... ->groupBy('user_id')->limit(4)... в подзапрос?

Igor 23.07.2023 18:28

Хорошо, тогда кажется, что лучше создать отношение lastPost hasOne и сделать это так. Разделение не кажется подходящим для вашего случая. https://stackoverflow.com/a/33780740/3955714

Igor 23.07.2023 19:09

Одним из возможных решений является то, что мы можем обмануть ваш ответ таким образом, что он будет умножать оба предельных значения, то есть limit(4x2). Сделав это, он будет соответствовать моему ожидаемому результату, но в конце в SQL будет выполняться запрос типа limit(8).

JS TECH 23.07.2023 19:15

Кроме того, ваш метод groupBy выдает мне эту ошибку. Оконная функция разрешена только в списке SELECT и предложении ORDER BY. Если можете ответить, дайте знать?

JS TECH 23.07.2023 19:18

Согласно ответу @Igor, я сделал его более удобным, поместив его в черты и локальные области видимости.

Приложение\Черты\WithEagerLimit.php

<?php

namespace App\Traits;

use Illuminate\Database\Eloquent\Builder;

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;

            // Illuminate\Database\Eloquent\Relations\Relation
            $query = call_user_func_array($callback, [$query]);

            // Illuminate\Database\Query\Builder
            $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');
        }]);
    }
}

Используйте черту WithEagerLimit на соответствующей модели.

Например:

class User extends Model {

    use \App\Traits\WithEagerLimit;
    
    //...

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Теперь вы можете связать withEagerLimit() с любым из ваших запросов, выполнив следующие действия:

Простая функция быстрой загрузки

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)

Условия для функции быстрой загрузки

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

Сложный SQL RAW-запрос по функции быстрой загрузки

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

Разбиение на страницы при нетерпеливой загрузке

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

Для тех, кто с нетерпением ждет ответа

Вы должны вызывать ->limit($value) или эквивалентный метод (например, take(), skip(), paginate() или аналогичную процедуру) в любых запросах, связанных с ->withEagerLimit($relationName, $callback) в основном запросе, а также в подзапросе.

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

Вы получаете преимущества использования этого признака только тогда, когда запрос дает достаточно результатов для обеих таблиц, т. е. пользователей и сообщений. В противном случае вы получите дополнительные результаты из коробки, потому что здесь мы сделали UsersLimit x PostsLimit, поэтому сохраните запрос соответствующим образом.

Другие вопросы по теме