Laravel从三元数据透视表中嵌套了关系

时间:2017-04-12 16:05:37

标签: php laravel eloquent many-to-many relationship

我有三个实体表,studentcoursesemester。它们通过三元数据透视表连接在一起 - 也就是说,每一行代表"学生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>

我意识到我可以创建一个手动完成并构建我想要的集合的帮助器,但我真的想将它作为单个关系实现,以便我可以在其他查​​询构建器表达式中流畅地使用它

1 个答案:

答案 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');

剩下的唯一问题是,由于它创建的行数与行数一样多,因此当学期包含多个课程时,会有重复的学期。我可能需要引入另一个自定义关系来展平这些重复值。