Laravel 模型操作中一次奇妙踩坑经历

@这是小豪的第十三篇文章

最近被 Laravel 模型中的一些小问题折腾的死去活来的,明明看着很清晰很明了的代码,却偏偏不能实现功能,现在带大家来切身经历一下这次奇妙的踩坑经历,代码看似很多,实则不多,大家别急着跑,哈哈。

准备

需求: 获取项目下的所有任务,且需要合并公共任务

逻辑关系:

  • 一个项目有很多任务
  • 一个项目有很多项目成员
  • 一个任务有一个执行人 (当任务类型为:1 的时候为公共事务)
  • 一个人有多个项目
  • 一个人有多个任务

前端所需数据格式如下:

{
    "user1": {
        "id": 1,
        "name": "Lhao",
        "email": "lhao@qq.com",
        "email_verified_at": null,
        "created_at": null,
        "updated_at": null,
        "pivot": {
            "project_id": 1,
            "user_id": 1
        },
        "tasks": [
            {
                "id": 1,
                "project_id": 1,
                "user_id": 1,
                "type": 0,
                "name": "task 1",
                "created_at": null,
                "updated_at": null
            }
            ...
        ]
    },
    "user2": {
            ...
    }
}

那我们现在来看看需要用到的各个模型,其中的各种对应关系我就不做讲解了哈,上面也有介绍,不太清楚的建议把模型关联再去细读一遍:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Project extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name',
    ];

    public function users()
    {  
        return $this->belongsToMany(User::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }
}
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'user_id',
        'name',
    ];

    const COMMON_TASK_TYPE = 1;

    public function scopeOfCommonTask(Builder $query)
    {
        return $query->where('type', self::COMMON_TASK_TYPE);
    }

    public function users()
    {  
        return $this->belongsTo(User::class);
    }

    public function project()
    {
        return $this->belongsTo(Project::class);
    }
}
...

class User extends Model
{
    ...

    public function projects()
    {  
        return $this->belongsToMany(Project::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }

    ...
}

开始

从上面的需求中大家可能会说,获取项目下的所有任务和公共事务直接通过:

$projectTasks = $project->tasks->merge(Task::ofCommonTask()->get())->groupBy('user_id');

这样不就可以了吗,但是这样有个问题就是数据格式不是前端所需要的,如果我们要转化成上面的格式的话,还需要获取用户数据然后将上面查询出来的数据塞进去,不太想这么干,不够优雅,哈哈,我打算通过项目获取到项目成员然后再加载任务数据,最后整合进公共任务,话不多说上代码:

public static function getProjectUserTasks(Project $project)
{
    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        $query->where('project_id', $project->id);
    }])->keyBy('name');

    // 不太清楚的 请看 scope 相关知识
    $commonTasks = Task::ofCommonTask()->get();

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        return $userTask;
    });

    return $userTasks;
}

看上面的代码是不是感觉很清爽,很直接,但是... 返回的数据是没有整合进 commonTask 的,这是为什么呢,明明 $userTask->tasks->merge($tasks) 也赋值了呀,问题出在哪里呢,我们测试一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        dd($userTask->tasks->toArray(), $userTask->toArray());

        return $userTask;
    });

    ....

具体的数据打印结果我就不贴出来了哈,占地方,哈哈,我直接说结果。

从打印的结果中可以看到 $userTask->tasks 中是有合并之后的数据的,但是 $userTask 还是原先的数据。这是为啥,我有点懵了,难道说 $userTask->tasks 操作是关联查询操作了?($userTask 是一个 User 对象集合,$userTask->tasks 会不会再次查询数据了?而不是直接获取的原有属性?),疑问出现了,我们就来测试看看:

    ...

    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        // $query->where('project_id', $project->id);
    }])->keyBy('name');

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        dd($userTask->tasks);
    });

    ....

通过对上面的测试发现,$userTask->tasks 是有携带上面查询条件的,所以说这个疑问排除了!

难道是集合属性不能这样赋值?我们再来测试一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->name = 111;

        return $userTasks;
    });

    ....

返回的结果是修改了的....

这就尴尬了,难道是对象集合中的非对象属性不能这样赋值?也不对呀,思来想去决定对对象本身做一个探索,直接在 map 中打印 $userTask :

Laravel 集合操作中一次奇妙踩坑经历

大家可以看到两个关键的属性:attributes、relations ,在实践中可以发现不管是 $userTask->name = "user**" 还是 $user->tasks = *** 的赋值操作都有对 attributes 做更改,这一点也可以从 Model 中的 __set 魔术方法中看到,其中是有调用一个 setAttribute 方法的,我们来看一下:

Laravel 集合操作中一次奇妙踩坑经历

Laravel 集合操作中一次奇妙踩坑经历

既然 attributes 被修改了,那究竟为啥在输出的时候只有他本身的属性有变更但是关联属性没有呢?

还记得我们刚才测试打印时候的 toArray 吗,就是他把对象集合转变成了一个数组,我们来看一下:

Laravel 集合操作中一次奇妙踩坑经历

明显看到 toArray 方法将 attributes 和 relations 转化成数组了,而且用的 array_merge 方法,大家知道相同 key 的时候,后面数组会覆盖前面数组,从前面的测试中可以看到 $userTask 中 attributes 是有变更,但是 relations 中的数据是没有发生任何变化的,这就可以解释为什么赋值 tasks 没有任何效果了,原有的数据覆盖掉了变更的数据。

所以我们现在要做的就是,对 relations 处理,那我们现在来看一下直接对 relations 处理是否有用:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->relations['tasks'] = $userTask->tasks->merge($commonTasks);

        return $userTasks;
    });

    ....

测试结果很显然是成功的,但是大家可能会发现直接操作 relations 或许有些不妥,别急,Laravel 也给我们提供了这样一个方法:

Laravel 集合操作中一次奇妙踩坑经历

现在我们把代码优化一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        return $user->setRelation('tasks', $user->tasks->merge($commonTasks));;
    });

    ....

大公告成,可以说很优雅,哈哈,大家可能会问,你这直接返回了没有调用 toArray 啊,数据是怎么合并的怎么转换的?大家知道在控制器中直接 return 的时候,是会直接转化为 Json 数据格式的,模型中也相对应的有这么一个方法:

Laravel 模型操作中一次奇妙踩坑经历

Laravel 模型操作中一次奇妙踩坑经历

一步步走下来发现,最终还是调用了 toArray 。所以嘛,这次踩坑算是跨过去了,哈哈。不知道大家有没有理解,有需要改进的地方大家在评论区留言噢。

特别鸣谢: zIym 同学 (咱俩一起跨的坑,哈哈)

结束语

其实吧最初我也没有想这么多,想了很多其它的解决办法,但是都是治根不治本,到头来发现自己对 Laravel 模型的工作原理还是不熟悉,只存在简单的应用上面,所以呀还是得追根溯源,并不是把时间都浪费在尝试上面,多看看源码,会有想不到的收获,哈哈。

本作品采用《CC 协议》,转载必须注明作者和本文链接
finecho # Lhao
本帖由系统于 4年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 12
$userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->jobs = $userTask->tasks->merge($commonTasks); // 这里名字不和关联方法名相同就OK
        return $userTask;
    });
4年前 评论
finecho

@fantasticcat 换名字是可以的,但是这篇文章主要是想告诉大家直接对关联属性赋值为什么不行,理解清楚之后,怎么去操作就更加明了了,哈哈

4年前 评论
feimn 3年前
finecho

@大毛 :see_no_evil:

4年前 评论

题外话,为什么前端会让你给user1 user2作为key的对象,这个真不是后端人员胡搞毛搞然后前端妥协了吗?

4年前 评论

感谢大佬分享 :+1:

4年前 评论

我猜是不是应用到tapd

3年前 评论

先问一下 你的模型对应关系是 projects对users 是多对多 projects对tasks是一对多 users对tasks是一对多吧 我按照你的写法来 发现结果对应不上啊 下面是我自己写了一下 但是公共任务类型没有融合进去 public function getProjectUserTasks($project) { $commonTask = Task::OfCommonTask()->get(); $usersTasks = $project->users->each(function ($userTask) use ($project, $commonTask) { return $usersTasks = $userTask->tasks->where('project_id', $project->id)->merge($commonTask)- >groupBy($userTask->id); })->values(); return $usersTasks; } 除了公共任务模型没融合上 其他的都没问题

3年前 评论
finecho

@feimn 有几个问题哈:

  1. each 只是一个普通的遍历操作,不应该用来接收值,也就是说你 each 里面的操作对结果是没有什么影响的(具体的可以看一下集合的文档),当然你看到 task 正常返回了,那是因为在 $userTask->task 的时候 task 模型关联到 user 里面去了
  2. $project->users->each (function ($userTask).... 这里的 $userTask 是不是换为 $user 会更合适一些
  3. $userTask->tasks->where ('project_id', $project->id) 这里的写法是查询用户的所有任务然后再通过集合的 where 方法去筛选,实属浪费哈,可以考虑一下 whereHas 或者 $userTask->tasks()->where()....->get()
  4. 文章你可能没仔细看,建议重新仔细看一遍噢
3年前 评论
finecho

@feimn 再吐槽一下,评论也是支持 MD 的,你贴出来的代码看的实在是费劲,哈哈

3年前 评论

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