【问题标题】:Laravel nested relationships from a ternary pivot table来自三元数据透视表的 Laravel 嵌套关系
【发布时间】:2017-09-08 12:41:46
【问题描述】:

我有三个实体表,studentcoursesemester。它们通过三元数据透视表链接在一起——也就是说,每一行代表“学生 X 在 Z 学期学习课程 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');
}

问题是,当我在我的 coursesBySemester 关系中调用 with('courses') 时,它会检索 所有 学生在该学期学习的所有课程。我只想要家长学生在那个学期学习的课程。

如您所见,我试图通过使用where('course_students.student_id', $this->id) 来约束这种关系,但$this->id 实际上并没有在关系的上下文中设置任何值。我也尝试过wherePivot 方法,但同样,我不知道如何根据父Student 模型的id 动态设置该约束。

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

【问题讨论】:

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


    【解决方案1】:

    我能够通过创建一个自定义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 的枢轴值匹配的课程。

    然后我可以在我的 Student 模型中定义一个关系 coursesBySemester

    /**
     * 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');
    

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

    【讨论】:

      猜你喜欢
      • 2017-07-22
      • 2018-05-02
      • 2014-09-29
      • 2014-08-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-09
      • 1970-01-01
      相关资源
      最近更新 更多