
本文介绍在 laravel 中使用 jenssegers/laravel-mongodb 扩展时,如何基于子模型(如 round)的数值字段(如 points)总和,高效查询并筛选父模型(如 game),涵盖关系定义、聚合查询实现及 mongodb 兼容性注意事项。
本文介绍在 laravel 中使用 jenssegers/laravel-mongodb 扩展时,如何基于子模型(如 round)的数值字段(如 points)总和,高效查询并筛选父模型(如 game),涵盖关系定义、聚合查询实现及 mongodb 兼容性注意事项。
在 Laravel + MongoDB 场景下,直接使用 DB::raw() 配合 SQL 风格的 JOIN 和 GROUP BY(如 MySQL 示例中的 leftJoin)无法正常工作,因为 MongoDB 是文档型数据库,不支持关系型 JOIN 操作,且 jenssegers/laravel-mongodb 的 Query Builder 并未实现 leftJoin 或原生 SQL 聚合语法的完整映射。若强行沿用关系型写法,将导致查询失败或返回空结果。
✅ 正确做法是利用 MongoDB 的聚合管道(Aggregation Pipeline),通过 $lookup(类似左连接)、$unwind 和 $group 实现“按游戏汇总所有回合积分”的逻辑。以下是完整、可运行的解决方案:
1. 确保模型关系已正确定义
在 Game.php 模型中声明一对多关系(虽 MongoDB 无外键约束,但为语义清晰与后续 Eloquent 方法兼容,仍建议定义):
// app/Models/Game.php
use Jenssegers\Mongodb\Eloquent\Model;
class Game extends Model
{
protected $collection = 'games'; // 显式指定集合名(可选)
public function rounds()
{
return $this->hasMany(Round::class, 'game_id');
}
}
// app/Models/Round.php
use Jenssegers\Mongodb\Eloquent\Model;
class Round extends Model
{
protected $collection = 'rounds';
}
2. 使用 MongoDB 聚合查询获取带总分的游戏列表
在控制器中调用 Game::raw() 执行原生聚合操作:
// 在控制器方法中,例如 GameController@getGamesByTotalPoints
use Illuminate\Http\JsonResponse;
public function getGamesByTotalPoints(): JsonResponse
{
$gamesWithTotalPoints = Game::raw(function ($collection) {
return $collection->aggregate([
// 步骤1:关联 rounds 集合(相当于 LEFT JOIN)
[
'$lookup' => [
'from' => 'rounds',
'localField' => '_id',
'foreignField' => 'game_id',
'as' => 'rounds'
]
],
// 步骤2:展开 rounds 数组(便于后续求和;若某 game 无 rounds,则 rounds 为空数组,$sum 会返回 0)
[
'$unwind' => [
'path' => '$rounds',
'preserveNullAndEmptyArrays' => true // 保留无回合的游戏
]
],
// 步骤3:按 game 分组,计算 points 总和,并保留原始 game 字段
[
'$group' => [
'_id' => '$_id',
'title' => ['$first' => '$title'],
'description' => ['$first' => '$description'],
'total_points' => ['$sum' => '$rounds.points']
]
],
// 步骤4(可选):投影为更友好的字段结构,兼容 Eloquent 返回习惯
[
'$project' => [
'_id' => '$_id',
'title' => 1,
'description' => 1,
'total_points' => 1
]
]
]);
});
return response()->json($gamesWithTotalPoints);
}
3. 进阶:添加条件筛选(如 total_points > 25)
可在 $group 后插入 $match 阶段:
[
'$match' => [
'total_points' => ['$gt' => 25]
]
]
⚠️ 注意事项
- 不要使用 leftJoin + DB::raw:该写法仅适用于 MySQL/PostgreSQL,MongoDB 驱动会忽略或报错。
- $unwind 的 preserveNullAndEmptyArrays => true 很关键:确保没有回合的游戏仍能出现在结果中(total_points 为 0)。
- 字段命名一致性:确认 Round 模型中存储 game_id 为 ObjectId 类型(如 ObjectId("...")),且与 Game::_id 类型匹配。
-
性能优化:对 game_id 和 points 字段建立复合索引可提升聚合速度:
// 在 Round 模型的 boot() 中或通过 Artisan 命令创建 Round::raw(function ($collection) { return $collection->createIndex(['game_id' => 1, 'points' => 1]); });
通过上述聚合管道方案,你不仅能准确获取每个 Game 对应的 total_points,还能无缝集成 MongoDB 的文档特性,真正实现高性能、可扩展的父子数据聚合查询。











