关于 Laravel 中的多态多对多关联时间戳更新 - Laravel 5.6

关于 Laravel 中的多态多对多关联时间戳更新 - Laravel 5.6

问题:多态关联如何更新父模型的时间戳?

laravel 5.6 模型关联文档 中提到“更新父级时间戳”,说了两种情况,一是 belongsTo,一是 belongsToMany,于是有网友提出关于多态关联的问题,在问题贴中,已经有人说明了源码中给出的方法:
file
MorphToMany 继承 BelongsToMany,以同步关联 \Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable::sync() 举例:

if (count($changes['attached']) ||
    count($changes['updated'])) {
    $this->touchIfTouching();
}

如果中间表发生了变化,将会触发 touchIfTouching() 方法,继续向下,查看源码:

public function touchIfTouching()
    {
        if ($this->touchingParent()) {
            $this->getParent()->touch();
        }

        if ($this->getParent()->touches($this->relationName)) {
            $this->touch();
        }
    }

代码中判断了两种情况。实际上,touchingParent() 方法判断该关联的被关联对象是否重写了 $touches 属性,使其包含“发起”该关联的对象;getParent()->touches() 判断该关联示例的“发起模型实例”是否重写了 $touches 属性,使其包含被关联对象。

以文档中的例子具化来说:

class Post extends Model
{
    /**
     * 获得此文章的所有标签。
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}
class Tag extends Model
{
    /**
     * 获得此标签下所有的文章。
     */
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
     *  获得此标签下所有的视频。
     */
    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

假设调用 Tag::posts(),得到 Relation instance(命名为 $relation),且 $relation->parent = $tag,$relation->related = $post,当运行至 touchIfTouching() 时:
第一步判断:

// 判断关联类是否包含本类的关联方法
protected function touchingParent()
{
    return $this->getRelated()->touches($this->guessInverseRelation());
}
// 推测出本类(即管理发起类)的在关联类中的关联方法名
protected function guessInverseRelation()
{
    return Str::camel(Str::plural(class_basename($this->getParent())));
}

若 Post 包含 tags() 方法,则 touchingParent() 返回 true,执行$this->getParent()->touch(),调用的是 Tag 继承自 HasTimeStamps 的 touch(),判断并更新时间戳。

第二步判断:
先判断 Tag 的 $touches 属性是否包含posts,若包含,则执行$this->touch(),调用的是 BelongsToMany::touch():

public function touch()
{
    $key = $this->getRelated()->getKeyName();

    $columns = [
        $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(),
    ];
    // 若中间表有 $tag 相关联的数据,则执行关联更新时间戳
    if (count($ids = $this->allRelatedIds()) > 0) {
        $this->getRelated()->newQuery()->whereIn($key, $ids)->update($columns);
    }
}

public function allRelatedIds()
{
    // 得出与 $tag 相关联的所有ID(此处 relatedPivotKey 在本例中为 taggable_id)
    return $this->newPivotQuery()->pluck($this->relatedPivotKey);
}

protected function newPivotQuery()
{
    $query = $this->newPivotStatement();

    foreach ($this->pivotWheres as $arguments) {
        call_user_func_array([$query, 'where'], $arguments);
    }

    foreach ($this->pivotWhereIns as $arguments) {
        call_user_func_array([$query, 'whereIn'], $arguments);
    }

    // 根据 foreignPivotKey(本例中即 'tag_id')和$tag->id比较,构造查询
    return $query->where($this->foreignPivotKey, $this->parent->{$this->parentKey});
}

Tips:
对于我贴出来的网友的回答,若担心在 Post 和 Tag 类中重写了 $touches 属性,导致对 Tag 更新连带的关联更新,可以在 save() 中传入 ['touch' => false] 即可。
下面贴出部分源码 Illuminate\Database\Eloquent\Model.php:

public function update(array $attributes = [], array $options = [])
{
    if (! $this->exists) {
        return false;
    }
    // update() 本质上也是调用 save() 并传入 options数组
    return $this->fill($attributes)->save($options);
}
public function save(array $options = [])
{
    // ...
    if ($saved) {
        // 此处传入 $options
        $this->finishSave($options);
    }
    // ...
}
protected function finishSave(array $options)
{
    // ...
    // 此处判断了 $options 中是否有 touch 参数且为 false
    // 一般开发者会忽略这个选项,也就导致了所谓的连带更新
    if ($this->isDirty() && ($options['touch'] ?? true)) {
        $this->touchOwners();
    }
    // ...
}

结语:
大体上如此,细节处可细看源码,我也是第一次阅读Laravel 5.6的模型关联源码,若有不细致、不清楚或者错误的地方,欢迎在评论中指出。

本作品采用《CC 协议》,转载必须注明作者和本文链接
坚持学习
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!