关于 Laravel 中的多态多对多关联时间戳更新 - Laravel 5.6
关于 Laravel 中的多态多对多关联时间戳更新 - Laravel 5.6
laravel 5.6 模型关联文档 中提到“更新父级时间戳”,说了两种情况,一是 belongsTo,一是 belongsToMany,于是有网友提出关于多态关联的问题,在问题贴中,已经有人说明了源码中给出的方法:
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 协议》,转载必须注明作者和本文链接