You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel 5.7带关联的Eloquent查询转Query Builder求助

用Query Builder实现Eloquent关联查询的等价方案

核心思路

Eloquent的with()是预加载机制,会执行多条查询;Query Builder要实现等价效果,需通过左连接+聚合函数打包关联数据,或用子查询获取单条关联,最后将结果映射成和Eloquent一致的结构。


分步实现(结合你提到的关联场景)

假设你的模型关联定义如下(匹配你给出的Post/Media/PlanDetail模型):

// PostModel.php
public function media()
{
    return $this->hasMany(MediaModel::class, 'post_id');
}

public function find_thumbnail_media()
{
    return $this->hasOne(MediaModel::class, 'post_id')->where('is_thumbnail', 1);
}

public function favorites()
{
    return $this->hasMany(PlanDetailModel::class, 'post_id', 'id');
}

1. 基础连接与查询构造

先通过左连接关联所有需要的表,注意一对一关联(缩略图)的条件:

$query = DB::table('posts')
    // 左连接media表(一对多)
    ->leftJoin('media', 'posts.id', '=', 'media.post_id')
    // 左连接缩略图media(带筛选条件的一对一)
    ->leftJoin('media as thumbnail_media', function ($join) {
        $join->on('posts.id', '=', 'thumbnail_media.post_id')
             ->where('thumbnail_media.is_thumbnail', 1);
    })
    // 左连接favorites表(一对多)
    ->leftJoin('plan_details as favorites', 'posts.id', '=', 'favorites.post_id');

2. 聚合打包关联数据

直接连接会导致主表数据重复,需按主表主键分组,用JSON聚合函数把关联数据打包成数组:

MySQL版本(5.7+)

$posts = $query
    ->select([
        'posts.*',
        // 打包media数据为JSON数组
        DB::raw('JSON_ARRAYAGG(JSON_OBJECT(
            "id", media.id,
            "url", media.url,
            "is_thumbnail", media.is_thumbnail
        )) as media'),
        // 缩略图为一对一,直接取字段并处理空值
        DB::raw('IFNULL(thumbnail_media.id, "") as thumbnail_id'),
        DB::raw('IFNULL(thumbnail_media.url, "") as thumbnail_url'),
        // 打包favorites数据为JSON数组
        DB::raw('JSON_ARRAYAGG(JSON_OBJECT(
            "id", favorites.id,
            "user_id", favorites.user_id,
            "created_at", favorites.created_at
        )) as favorites')
    ])
    ->groupBy('posts.id')
    ->get();

PostgreSQL版本

将JSON_ARRAYAGG替换为json_agg,JSON_OBJECT替换为json_build_object即可。

3. 映射结果结构(对齐Eloquent格式)

Query Builder返回的是StdClass对象,需将字段转换为Eloquent的关联结构:

$formattedPosts = $posts->map(function ($post) {
    // 处理media:解析JSON并过滤空数据
    $post->media = json_decode($post->media, true) ?: [];
    $post->media = array_filter($post->media, fn($item) => !empty($item['id']));

    // 处理find_thumbnail_media:组装成关联对象
    $post->find_thumbnail_media = !empty($post->thumbnail_id) ? (object)[
        'id' => $post->thumbnail_id,
        'url' => $post->thumbnail_url,
        'is_thumbnail' => 1
    ] : null;

    // 处理favorites:解析JSON并过滤空数据
    $post->favorites = json_decode($post->favorites, true) ?: [];
    $post->favorites = array_filter($post->favorites, fn($item) => !empty($item['id']));

    // 移除临时字段
    unset($post->thumbnail_id, $post->thumbnail_url);

    return $post;
});

性能优化备选方案

如果关联表数据量较大,聚合函数可能产生性能瓶颈,可采用分查询手动匹配(模拟Eloquent预加载逻辑):

// 第一步:查询主表+子查询获取缩略图
$posts = DB::table('posts')
    ->select([
        'posts.*',
        '(SELECT url FROM media WHERE post_id = posts.id AND is_thumbnail = 1 LIMIT 1) as thumbnail_url'
    ])
    ->get();

// 第二步:批量查询关联数据
$postIds = $posts->pluck('id')->toArray();
$mediaList = DB::table('media')->whereIn('post_id', $postIds)->get()->groupBy('post_id');
$favoritesList = DB::table('plan_details')->whereIn('post_id', $postIds)->get()->groupBy('post_id');

// 第三步:手动匹配关联数据
$posts->each(function ($post) use ($mediaList, $favoritesList) {
    $post->media = $mediaList->get($post->id, collect());
    $post->find_thumbnail_media = $post->thumbnail_url ? (object)[
        'url' => $post->thumbnail_url,
        'is_thumbnail' => 1
    ] : null;
    $post->favorites = $favoritesList->get($post->id, collect());
    unset($post->thumbnail_url);
});

内容的提问来源于stack exchange,提问作者Ngọc Đồ Đinh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:25:19