Как @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;
});
}
Кто-нибудь знает, куда я должен положить глаза, чтобы свернуть этот пакет?






Я думаю, вам нужно удалить 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();
Попробуйте с... ->groupBy('user_id')->limit(4)... в подзапрос?
Хорошо, тогда кажется, что лучше создать отношение lastPost hasOne и сделать это так. Разделение не кажется подходящим для вашего случая. https://stackoverflow.com/a/33780740/3955714
Одним из возможных решений является то, что мы можем обмануть ваш ответ таким образом, что он будет умножать оба предельных значения, то есть limit(4x2). Сделав это, он будет соответствовать моему ожидаемому результату, но в конце в SQL будет выполняться запрос типа limit(8).
Кроме того, ваш метод groupBy выдает мне эту ошибку. Оконная функция разрешена только в списке SELECT и предложении ORDER BY. Если можете ответить, дайте знать?
Согласно ответу @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, поэтому сохраните запрос соответствующим образом.
Ага! Но вопрос в том, как данные будут устроены/взяты. Вместо того, чтобы получать 4 сообщения для каждого пользователя, он будет получать 2-2 сообщения для каждого пользователя. Итак, это не то, что я ожидал.