Ограничьте связанные записи в полиморфных отношениях "многие ко многим" с 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));
        });