Ограничьте связанные записи в полиморфных отношениях "многие ко многим" с Laravel
Я использую Laravel 5.1, и мне нужно ограничить количество связанных записей, которые я вытягиваю, используя полиморфные отношения "многие-ко-многим" .
То, что я хотел бы сделать, это получить список категорий по parent_id. Для каждой категории я бы хотел только потянуть четыре сообщения.
У меня есть работа с кодом ниже, но это приводит к кучке дополнительных запросов. Я хотел бы просто нажать базу данных один раз, если это возможно. Я бы хотел использовать Laravel/Eloquent framework, если это вообще возможно, но я открыт для того, что работает на этом этапе.
@foreach ($categories as $category)
@if ($category->posts->count() > 0)
<h2>{{ $category->name }}</h2>
<a href="/style/{{ $category->slug }}">See more</a>
{-- This part is the wonky part --}
@foreach ($category->posts()->take(4)->get() as $post)
{{ $post->body }}
@endforeach
@endif
@endforeach
PostsController
public function index(Category $category)
{
$categories = $category->with('posts')
->whereParentId(2)
->get();
return view('posts.index')->with(compact('categories'));
}
База данных
posts
id - integer
body - string
categories
id - integer
name - string
parent_id - integer
categorizables
category_id - integer
categorizable_id - integer
categorizable_type - string
Модель публикации
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function categories()
{
return $this->morphToMany('App\Category', 'categorizable');
}
Модель категории
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
public function category()
{
return $this->belongsTo('App\Category', 'parent_id');
}
public function subcategories()
{
return $this->hasMany('App\Category', 'parent_id')->orderBy('order', 'asc');
}
public function posts()
{
return $this->morphedByMany('App\Post', 'categorizable');
}
Я видел несколько ссылок на это в Интернете, но ничего, что действительно сработало для меня.
Я пробовал это решение без везения.
$categories = $category->with('posts')
->whereParentId(2)
->posts()
->take(4)
->get();
Я рассмотрел это решение от Jarek в SoftOnTheSofa, но это для отношений hasMany, и, честно говоря, немного выше моего уровня sql для меня, чтобы адаптировать это для моих нужд.
Изменить
Я добавил github repo для этой настройки, если это полезно кому-либо.
Ответы
Ответ 1
Изменить модель своей категории
public function fourPosts() {
// This is your relation object
return $this->morphedByMany('App\Post', 'categorizable')
// We will join the same instance of it and will add a temporary incrementing
// Number to each post
->leftJoin(\DB::raw('(' . $this->morphedByMany('App\Post', 'categorizable')->select(\DB::raw('*,@post_rank := IF(@current_category = category_id, @post_rank + 1, 1) AS post_rank, @current_category := category_id'))
->whereRaw("categorizable_type = 'App\\\\Post'")
->orderBy('category_id', 'ASC')->toSql() . ') as joined'), function($query) {
$query->on('posts.id', '=', 'joined.id');
// And at the end we will get only posts with post rank of 4 or below
})->where('post_rank', '<=', 4);
}
Затем в вашем контроллере все категории, которые вы получаете с этим
$categories = Category::whereParentId(1)->with('fourPosts')->get();
Будет только четыре должности. Чтобы проверить это, сделайте это (помните, что теперь вы будете загружать свои сообщения с помощью метода fourPosts
, поэтому вам нужно получить доступ к четырем сообщениям с этим свойством):
foreach ($categories as $category) {
echo 'Category ' . $category->id . ' has ' . count($category->fourPosts) . ' posts<br/>';
}
Короче вы добавляете подзапрос к объекту морфинга, который позволяет нам назначать временный инкрементный номер для каждого сообщения в категории. Затем вы можете просто получить строки, у которых это временное число меньше или равно 4:)
Ответ 2
Это работает для вас?:
$categories = $category->with(['posts' => function($query)
{
$query->take(4);
})
->whereParentId(2)
->get();
Ответ 3
Я думаю, что для большинства кросс-СУБД это будет использование union all. Может быть, что-то вроде этого:
public function index(Category $category)
{
$categories = $category->whereParentId(2)->get();
$query = null;
foreach($categories as $category) {
$subquery = Post::select('*', DB::raw("$category->id as category_id"))
->whereHas('categories', function($q) use ($category) {
$q->where('id', $category->id);
})->take(4);
if (!$query) {
$query = $subquery;
continue;
}
$query->unionAll($subquery->getQuery());
}
$posts = $query->get()->groupBy('category_id');
foreach ($categories as $category) {
$categoryPosts = isset($posts[$category->id]) ? $posts[$category->id] : collect([]);
$category->setRelation('posts', $categoryPosts);
}
return view('posts.index')->with(compact('categories'));
}
И тогда вы сможете прокручивать категории и свои сообщения в представлении. Не обязательно красивое решение, но оно сократило бы до 2 запросов. Сокращение до 1 запроса, вероятно, потребует использования оконных функций (в частности, row_number()
), которые MySQL не поддерживает без каких-либо трюков для эмуляции (Подробнее об этом здесь.). Я был бы рад, если бы оказался ошибочным.
Ответ 4
Вот "половинный ответ". Пол, который я вам даю, - это показать вам код MySQL. Пол, который я не могу вам дать, переводит его в Laravel.
Вот пример поиска 3 самых густонаселенных городов в каждой провинции Канады:
SELECT
province, n, city, population
FROM
( SELECT @prev := '', @n := 0 ) init
JOIN
( SELECT @n := if(province != @prev, 1, @n + 1) AS n,
@prev := province,
province, city, population
FROM Canada
ORDER BY
province ASC,
population DESC
) x
WHERE n <= 3
ORDER BY province, n;
Более подробную информацию и объяснение можно найти в моем блоге по групповому максимуму.
Ответ 5
Я думаю, что лучший вариант (без использования необработанных запросов) - это настроить настраиваемый метод, который будет использоваться при загрузке.
Итак, в модели Category
вы можете сделать что-то вроде:
<?php
public function my_custom_method()
{
return $this->morphedByMany('App\Post', 'categorizable')->take(4);
}
И затем используйте этот метод при активной загрузке:
<?php
App\Category::with('my_custom_method')->get();
Обязательно используйте my_custom_method()
вместо posts()
в вашем представлении:
$category->my_custom_method()
Ответ 6
Вы можете попробовать это, это работает для меня!
$projects = Project::with(['todo'])->orderBy('id', 'asc')->get()->map(function($sql) {
return $sql->setRelation('todo', $sql->todo->take(4));
});