【问题标题】:Join subquery in Laravel model在 Laravel 模型中加入子查询
【发布时间】:2020-05-02 09:10:14
【问题描述】:

请为这个问题建议一个更好的标题。我无法正确命名我的问题。

背景

我正在创建一个供个人使用的漫画数据库,以跟踪我的漫画阅读。每部漫画都属于一个系列。每部漫画都有发布日期。系列的发布日期是相应漫画的首次发布。为此,我有一个雄辩的函数seriesByDate()

class Series extends Model
{
  public $timestamps = false;
  protected $primaryKey = 'series_id';
  protected $fillable = ['series_name', 'publisher_id'];

  public function publisher()
  {
    return $this->belongsTo(Publisher::class, 'publisher_id');
  }

  public function comics()
  {
    return $this->hasMany(Comic::class, 'series_id', 'series_id');
  }

  // instead of saving the release date of a complete series
  // we look for the first comic in this series and get the
  // comic's release date.
  public static function seriesByDate()
  {
    $firstRelease = DB::table('comics')
      ->select('series_id', DB::raw('MIN(comic_release_date) as first_release'))
      ->groupBy('series_id');

    $seriesByDate = DB::table('series')
      ->leftJoinSub($firstRelease, 'first_release', function ($join) {
        $join->on('series.series_id', '=', 'first_release.series_id');
      })
      ->join('publishers', 'publishers.publisher_id', '=', 'series.publisher_id')
      ->select('series.series_id', 'series.series_name', 'first_release', 'publishers.publisher_name')
      ->get();

    return $seriesByDate;
  }

我想要什么

我希望 release_date 在我的 Series 模型中永久存在。含义:当我执行App\Series::all() 时,我已经希望将 release_date 作为我返回的数据中的一列。类似于App\Series::with('publishers')->get()

使用上面的解决方案,我必须明确执行App\Series::seriesByDate()

这甚至可能吗?可以给个提示吗?

编辑/更新

@Musa 的链接视频展示了如何在模型中正确执行此操作:https://stackoverflow.com/a/61558482/5754486

【问题讨论】:

    标签: mysql laravel laravel-7


    【解决方案1】:
    • 你不能。这没有什么魔法。您最终可能会编写自己的自定义关系,但这会不必要地复杂,只是为了拥有漂亮的related/访问器。这两种解决方案的性能都不是很好。

    • 不知道为什么选择这样的结构。没有任何进一步的上下文/解释,我强烈建议您在 Series 模型中也直接有一个 release_date 列。这将比您当前的结构快得多。

    • 如果您仍想坚持该结构,我会亲自检索release_date“php 端”而不是“数据库端”:

    $series = App\Series::query()
        ->with([
            'publishers',
            'comics' => function ($query) {
                $query->orderBy('created_at');
            },
        ])
        ->get();
    
    foreach ($series as $serie) {
        $serieTitle = $serie->title;
        $releaseDate = $serie->comics->first()->created_at;
    
        echo $serieTitle.' was first released '.$releaseDate->diffForHumans().'<br/>';
    }
    
    

    (未测试) 唯一的缺点是它将返回一个“系列”拥有的每个“漫画”的集合。如果每个系列没有 10k 漫画,并且每页没有加载 1k 系列,那应该没问题。无论如何,这看起来比您的 seriesByDate 方法更优雅、更优化/更快。

    编辑:另外,我相信您应该在 Laracon 2018 上观看 Jonathan Reinink 撰写的“使用 Eloquent 进行高级查询”。他讨论了您需要的子查询。我 100% 确信你会找到最好和最优化的 Eloquent 子查询,可以为你想要实现的目标伪造:https://vimeo.com/showcase/7060635/video/255049572

    【讨论】:

    • 有效点。我的想法是,我永远不必关心系列的发布日期。如果我有第二列 release_date 我需要一个额外的逻辑来保持 Comics.release_date 和 series.release_date 更新。谢谢你的视频。我会去看的。
    • 在这种情况下,您可能会考虑在 Comic 模型上使用模型观察器。每次您在 Comic 模型上执行创建/更新时,它都会自动为您更新 Series 模型上的 release_date 列,具体取决于您自己的逻辑。 laravel.com/docs/7.x/eloquent#observers
    • 实际上链接的视频很棒,它准确地显示了我的用例的解决方案。我的基本想法没有错,只是执行得很差:-) Reinink 还构建了一个子选择,急切地加载它并使用范围将它绑定到模型。非常好。谢谢。
    【解决方案2】:

    你可以定义一个accessor 然后附加值

    class Series extends Model
    {
        protected $appends = ['series_date'];
    
        public function getSeriesDateAttribute()
        {
            return self::seriesByDate();
            //OR build 'seriesByDate' manually, returning whatever you like.
        }
    }
    

    【讨论】:

    • 听起来很有希望,但我做错了什么? protected $appends = ['test_var']; public function getTestVarAttribute() { return "hi"; }
    • 似乎$appends 被忽略/未使用。 tinker shell 的输出:&gt;&gt;&gt; App\Series::get()[0] =&gt; App\Series {#3224 series_id: 1, series_name: "Batman", publisher_id: 1, }
    • 我不确定为什么即使我使用toArray,它也不会出现在修补程序中。也许这是因为 tinker 总是返回模型实例。如果您不确定它是否已添加到附加列表中,请尝试 toArraytoJson 它应该是可见的
    • 其实这也是一个有效的解决方案。谢谢。只是我做这个讨厌的加入/子查询的方式很混乱。
    猜你喜欢
    • 2018-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-24
    • 2015-07-14
    • 2013-08-07
    • 1970-01-01
    相关资源
    最近更新 更多