我有三个实体表,student
,course
和semester
。它们通过三元数据透视表连接在一起 - 也就是说,每一行代表"学生X在学期Z&#34中学习Y课程:
# Table course_students
| student_id | semester_id | course_id |
|------------|-------------|-----------|
| 18 | 4 | 80 |
| 18 | 8 | 64 |
| 18 | 8 | 60 |
由此,我想构建一个嵌套集合,以便:
因此,对于上表,我想调用类似Student::find(18)->with('coursesBySemester')
的内容并获得一个类似的集合:
{
"id": 18,
"first_name": "Wesley",
"last_name": "Snipes",
"email": "wes@expendables.com",
"semesters": [
{
"id": 4,
"name": "Fall 2014",
"pivot": {
"student_id": 18,
"semester_id": 4
},
"courses": [
{
"id": 80,
"title": "Game Theory",
"pivot": {
"semester_id": 4,
"course_id": 80,
"student_id": 18
}
},
]
},
{
"id": 8,
"name": "Fall 2016",
"pivot": {
"student_id": 18,
"semester_id": 8
},
"courses": [
{
"id": 64,
"title": "Introduction to Calculus with Applications",
"pivot": {
"semester_id": 8,
"course_id": 64,
"student_id": 18
}
},
{
"id": 60,
"title": "Introduction to Finite Math 1",
"pivot": {
"semester_id": 8,
"course_id": 60,
"student_id": 18
}
}
]
}
]
}
我可以通过Student
模型中定义的以下关系获得大部分内容:
/**
* Load a collection of semesters during which this student was enrolled in at least one course, and the courses that they took in each semester
*/
public function coursesBySemester()
{
return $this->belongsToMany('UserFrosting\Sprinkle\Btoms\Model\Semester', 'course_students')
->with(['courses' => function ($query) {
return $query->where('course_students.student_id', $this->id);
}])
->groupBy('semester_id');
}
Semester
模型定义了以下关系:
/**
* Lazily load a collection of courses that were taken in this semester.
*/
public function courses()
{
return $this->belongsToMany('UserFrosting\Sprinkle\Btoms\Model\Course', 'course_students')->withPivot('student_id');
}
问题在于,当我在with('courses')
关系中调用coursesBySemester
时,它会检索所有学生在该学期中学习的所有课程。我只想要家长学生在那个学期学习的课程。
正如您所看到的,我尝试使用where('course_students.student_id', $this->id)
约束该关系,但$this->id
实际上并未在关系的上下文中设置任何值。我还尝试了wherePivot
方法,但我不知道如何根据父id
模型的Student
动态设置该约束。< / p>
我意识到我可以创建一个手动完成并构建我想要的集合的帮助器,但我真的想将它作为单个关系实现,以便我可以在其他查询构建器表达式中流畅地使用它
答案 0 :(得分:0)
我可以通过创建自定义Relation
类来解决此问题。
<?php
// MyProject/Model/Relations/BelongsToManyConstrained.php
namespace MyProject\Model\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class BelongsToManyConstrained extends BelongsToMany
{
/**
* @var The pivot foreign key on which to constrain the result sets for this relation.
*/
protected $constraintKey;
/**
* Create a new belongs to many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $table
* @param string $foreignKey
* @param string $relatedKey
* @param string $constraintKey
* @param string $relationName
* @return void
*/
public function __construct(Builder $query, Model $parent, $table, $foreignKey, $relatedKey, $constraintKey, $relationName = null)
{
$this->constraintKey = $constraintKey;
parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName);
}
/**
* Match the eagerly loaded results to their parents, constraining the results by matching the values of $constraintKey
* in the parent object to the child objects.
*
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array
*/
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
// Once we have an array dictionary of child objects we can easily match the
// children back to their parent using the dictionary and the keys on the
// the parent models. Then we will return the hydrated models back out.
foreach ($models as $model) {
$pivotId = $model->getRelation('pivot')->{$this->constraintKey};
if (isset($dictionary[$key = $model->getKey()])) {
$items = $this->findMatchingPivots($dictionary[$key], $pivotId);
$model->setRelation(
$relation, $this->related->newCollection($items)
);
}
}
return $models;
}
/**
* Filter an array of models, only taking models whose $constraintKey value matches $pivotValue.
*
* @param mixed $pivotValue
* @return array
*/
protected function findMatchingPivots($items, $pivotValue)
{
$result = [];
foreach ($items as $item) {
if ($item->getRelation('pivot')->{$this->constraintKey} == $pivotValue) {
$result[] = $item;
}
}
return $result;
}
}
现在,在我的Semester
课程中,我可以定义这种关系:
/**
* Lazily load a collection of courses that were taken in this semester by related students.
*/
public function coursesForStudent()
{
$instance = $this->newRelatedInstance('MyProject\Model\Course');
$foreignKey = $this->getForeignKey();
$relatedKey = $instance->getForeignKey();
$query = new BelongsToManyConstrained(
$instance->newQuery(), $this, 'course_students', $foreignKey, $relatedKey, 'student_id', 'courses'
);
// Need to make sure we add the `student_id` pivot for BelongsToManyConstrained to match
$query = $query->withPivot('student_id');
return $query;
}
请注意,我已将student_id
传递给BelongsToManyConstrained
的构造函数。这告诉关系它应该只检索其student_id
的透视值与父对象的student_id
的透视值匹配的课程。
然后,我可以在coursesBySemester
模型中定义关系Student
:
/**
* Lazily load a collection of semesters during which this student was enrolled in a course.
*/
public function coursesBySemester()
{
return $this->belongsToMany('MyProject\Model\Semester', 'course_students')
->with('coursesForStudent');
}
现在我可以通过以下方式获得我想要的嵌套结果集:
$student = Student::find(1)->with('coursesBySemester');
剩下的唯一问题是,由于它创建的行数与行数一样多,因此当学期包含多个课程时,会有重复的学期。我可能需要引入另一个自定义关系来展平这些重复值。