使用 Laravel 限制多态多对多关系中的相关记录

Posted

技术标签:

【中文标题】使用 Laravel 限制多态多对多关系中的相关记录【英文标题】:Limit related records in polymorphic many-to-many relationship with Laravel 【发布时间】:2015-09-11 09:55:27 【问题描述】:

我使用的是 Laravel 5.1,我需要限制使用 polymorphic many-to-many relationship 提取的相关记录的数量。

我想做的是按 parent_id 获取类别列表。对于每个类别,我只想拉四个帖子。

我已经使用下面的代码进行了此操作,但它会导致一堆额外的查询。如果可能的话,我想只打一次数据库。如果可能的话,我想使用 Laravel/Eloquent 框架,但我对目前可行的方法持开放态度。

@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');
    

我在网上看到了许多指向此的链接,但没有一个对我真正有用。

I have tried this solution 没有任何运气。

$categories = $category->with('posts')
->whereParentId(2)
->posts()
->take(4)
->get();

I have looked into this solution 来自 SoftOnTheSofa 的 Jarek,但它是针对 hasMany 关系的,老实说,这有点超出了我的 sql 技能,我无法根据自己的需要进行调整。

编辑

我为此设置添加了github repo,如果它对任何人有用的话。

【问题讨论】:

@TaylorOtwell 有什么想法吗? @jarek-tkaczyk 有什么想法吗? @matt-stauffer 有什么想法吗? 我认为您的主要问题是,这将无法通过急切加载来完成。据我了解,急切加载实际上会执行两个查询 - 一个获取所有结果,一个获取所有关系。所以Cats::with('posts')-&gt;all(); 会做SELECT * FROM `cats``` and then gather all the IDs retrieved from that query and then craft a new one SELECT * FROM posts WHERE cat_id IN ([long list of IDs])``,然后它会遍历那个结果集并将模型分成他们所属的父母到。显然这是针对 BT 的,但我认为任何关系都遵循同样的规则。 因此,放置任何 -&gt;take() 会将总结果集限制为 4,而不是做你想做的事。您将不得不手动执行此操作。但是,您可以使用 Laravel 自己的查询构建器来执行原始 SQL 查询,而不会产生原始查询的丑陋。这对你来说是可以接受的还是你需要使用 Eloquent 的魔法来实现你想要的? 【参考方案1】:

这对你有用吗?:

$categories = $category->with(['posts' => function($query)
    
        $query->take(4);
    )
    ->whereParentId(2)
    ->get();

【讨论】:

不幸的是,这将查询限制为总共 4 个帖子,而不是每个类别 4 个帖子,但谢谢。 我想除了在单独的 SQL 中查询每个类别的帖子之外别无他法。您可以通过缓存 5 分钟过期时间的帖子来克服 mysql 的开销。【参考方案2】:

我认为最好的选择(不使用原始查询)是设置一个自定义方法以用于预加载。

因此,在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()

【讨论】:

所以这个方法也是,只给我总共 4 个帖子,而不是每个类别 4 个。 它会根据需要为每个类别提供四个帖子。 我刚刚测试了它,它没有。也许它与与自身相关的 Category 模型有关? 我想我对你认为的用法有点不清楚。我在最后更新了我的答案,提供了更多信息。 如果你看我的问题,基本上我就是这样做的。这种方法的问题是它没有利用预先加载,并且您会遇到一个页面的多个查询的 n+1 问题。【参考方案3】:

我认为最跨 DBMS 的方法是使用 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()),如果没有一些技巧来模拟它(More on that here.),MySQL 是不支持的。不过,我很高兴被证明是错误的。

【讨论】:

这看起来可能有效,但你已经为 hasMany 关系设置了这个。让我看看我能不能让它工作。 虽然我认为你的方法有一些东西,但没有运气让它发挥作用。 我很好奇,它有什么问题?我还没有机会实际设置表格并对其进行真正测试,但这个概念对我来说似乎是合理的。 两个主要问题是,posts 表没有 category_id 列,并且 unionAll 没有获取数据库表名。我为这个问题设置了一个 github 存储库,并整合了您的建议。 github.com/whoacowboy/example-laravel-many-to-many 啊,对,我有点忽略了这一点,把 unionAll 搞砸了。我为多对多关系编辑它并修复了 unionAll。我认为它现在应该可以工作了。【参考方案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;

更多细节和解释见my blog on groupwise-max。

【讨论】:

【参考方案5】:

编辑您的类别模型

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/>';

简而言之,您向 morph 对象添加一个子查询,允许我们为类别中的每个帖子分配临时递增数字。然后你可以得到这个临时数小于或等于 4 的行:)

【讨论】:

感谢您的回答。这个错在左边加入。这是否也考虑了多对多的关系? @whoacowboy 好的,我使用了你的 git repo(非常有用,我们需要更多的 OP),并在我的本地环境上做了一些测试。请查看我编辑的答案,它非常完美:) @whoacowboy 是的,所有的关系都会好的,我们只修改拉帖子的查询,而不是实际的对象。如果这对您有用,请告诉我,我们会很高兴知道反馈。【参考方案6】:

你可以试试这个,对我有用!

$projects = Project::with(['todo'])->orderBy('id', 'asc')->get()->map(function($sql) 
            return $sql->setRelation('todo', $sql->todo->take(4));
        );

【讨论】:

以上是关于使用 Laravel 限制多态多对多关系中的相关记录的主要内容,如果未能解决你的问题,请参考以下文章

Laravel多态多对多关系数据透视表与另一个模型的关系

HasManyThrough 具有多态和多对多关系

Laravel 关系多对多按外键计数相关数据

两个多对多相关表之间的Laravel雄辩关系

如何在多对多关系 Laravel 中检索所有相关模型?

Laravel一张表连接到另外两个由多对多的关系