news 2026/6/8 20:16:15

laravel的延迟加载的源码解读的庖丁解牛

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
laravel的延迟加载的源码解读的庖丁解牛

在 Laravel 中,“延迟加载”通常指两个层面的概念,但源码机制截然不同:

  1. Eloquent 关联关系的延迟加载(最常用,也是性能陷阱所在):访问$user->posts时才去查数据库。
  2. 服务容器的延迟加载:服务只有在第一次被make()时才实例化。

鉴于前文已深入探讨过容器,这里我们聚焦于Eloquent 关联关系的延迟加载,这是 Laravel ORM 中最具魔力也最危险的特性。

它的本质是:**延迟加载是一种“用时间换空间/便利性”的策略。它通过魔术方法 (__get)拦截属性访问,在运行时动态发起数据库查询,将关联数据填充到模型中。

  • 核心矛盾:用户希望像访问普通属性一样访问关联数据($user->posts),但关联数据不在内存中,而在数据库里。
  • 解决方案:当代码尝试读取不存在的属性时,Laravel 检查这是否是一个定义的关联关系。如果是,它立即执行 SQL 查询,获取结果,缓存到模型内部,然后返回。
  • 核心逻辑别把延迟加载当成“智能预知”。它是“被动触发”。你不去摸它,它就不动;你一摸,它就跑去数据库搬砖。如果在循环里摸,它就跑断腿(N+1)。

如果把延迟加载比作点菜

  • 预加载 (Eager Loading):是套餐。上桌时所有菜都齐了。吃的时候不用等。
  • 延迟加载 (Lazy Loading):是单点。你坐下时只有主菜。当你喊“我要汤” ($user->posts) 时,服务员才去厨房现做(查库)。如果你每喝一口汤都喊一次,服务员会累死。
  • 核心逻辑延迟加载的核心在于拦截器 (Interceptor)一次性填充 (One-time Hydration)

一、触发机制:__get魔术方法

一切始于你对模型属性的访问。

1. 入口:Model::__get()
  • 代码位置Illuminate\Database\Eloquent\Model::__get($key)
  • 场景:当你调用$user->posts时,如果posts不是模型的直接属性(即在$attributes数组中不存在),PHP 会自动调用__get('posts')
2. 判断逻辑
  • 步骤
    1. 检查属性array_key_exists($key, $this->attributes)?如果是,直接返回值。
    2. 检查关联method_exists($this, $key)?或者更准确地说,检查是否有名为$key的方法,且该方法返回Relation对象。
    3. 触发加载:如果确认为关联,调用$this->getRelationshipFromMethod($key)

💡 核心洞察__get是延迟加载的开关。它将“属性访问”语义转换为“方法调用”语义。


二、关联解析流程:从方法到查询

1. 获取关联对象:getRelationshipFromMethod()
  • 代码位置Model::getRelationshipFromMethod($method)
  • 动作
    1. 调用$this->$method()。注意,这里是调用方法,而不是访问属性。
    2. 例如:$this->posts()返回一个HasMany关系对象。
    3. 关键点:此时还没有执行 SQLHasMany对象只是持有外键信息和查询构建器。
2. 执行查询:getResults()
  • 代码位置Illuminate\Database\Eloquent\Relations\Relation::getResults()
  • 动作
    1. HasMany继承自Relation
    2. 调用$this->query->get()
    3. 这里触发了Query Builder的执行,生成 SQL 并查询数据库。
    4. 返回Collection结果。
3. 存入模型:setRelation()
  • 代码位置Model::setRelation($relation, $value)
  • 动作
    1. 将查询结果存入模型的$relations数组:$this->relations[$method] = $value
    2. 价值:下次再访问$user->posts时,__get会先检查$relations,发现已有数据,直接返回,不再查库

💡 核心洞察延迟加载只发生在第一次访问。后续访问都是内存读取。这就是为什么它叫“加载”,而不是“查询”。


三、源码关键路径图解

$user->posts (Access Property) | v Model::__get('posts') | +-- Is 'posts' in $attributes? NO | +-- Is 'posts()' a method returning Relation? YES | v Model::getRelationshipFromMethod('posts') | v Call $this->posts() --> Returns HasMany Object (No SQL yet) | v HasMany::getResults() | v Builder::get() --> EXECUTES SQL: SELECT * FROM posts WHERE user_id = ? | v Returns Collection | v Model::setRelation('posts', $collection) <-- Caches in $relations | v Return $collection to user

四、N+1 问题的根源:源码视角的悲剧

为什么延迟加载会导致 N+1?

场景
$users=User::all();// 1 queryforeach($usersas$user){echo$user->posts->count();// N queries}
源码分析
  1. User::all()返回 100 个User模型实例。此时它们的$relations数组是空的。
  2. 进入循环。
  3. 第一次迭代:
    • 访问$user->posts
    • 触发__get
    • 检查$relations['posts']->
    • 执行getRelationshipFromMethod->查库(Query #2)。
    • 缓存结果。
  4. 第二次迭代:
    • 访问另一个$user对象的->posts
    • 触发__get
    • 检查该对象$relations['posts']->(因为每个模型实例是独立的)。
    • 执行getRelationshipFromMethod->查库(Query #3)。
  5. …重复 100 次。

💡 核心洞察N+1 的本质是对象隔离性。每个模型实例不知道其他实例的需求,因此各自为战,独立发起查询。


五、对比:预加载 (Eager Loading) 如何打破魔咒?

1. 入口:with()
  • 代码User::with('posts')->get()
  • 机制
    1. 先查询所有 Users。
    2. 收集所有 User 的 ID。
    3. 执行一次查询:SELECT * FROM posts WHERE user_id IN (1, 2, ... 100)
    4. 关键步骤MatchThroughRelations
      • 遍历查询结果。
      • 根据user_id找到对应的User模型实例。
      • 调用$user->setRelation('posts', $matchedPosts)
    5. 此时,所有 User 模型的$relations['posts']都已填满。
2. 访问时
  • 循环中访问$user->posts
  • __get检查$relations->有数据
  • 直接返回,零查询

🚀 总结:原子化“Laravel 延迟加载”全景图

维度关键点
本质基于__get拦截的按需数据库查询机制
核心触发访问未加载的关联属性 ->__get->getRelationshipFromMethod
缓存机制结果存入$model->relations数组,避免重复查询
性能陷阱N+1 问题:循环中访问不同实例的关联,导致多次查询
解决方案预加载 (with()):提前批量查询并填充$relations
源码核心类Model,HasMany(etc.),Relation,Builder
PHP 隐喻Ordering Food on Demand (Lazy) vs. Buffet Set (Eager)
公式Load = (Intercept × Query) ^ Cache

终极心法

延迟加载的本质,是“懒惰的智慧”。
它不预先做任何事,直到被需要。
这种懒惰在单次访问时是高效的,但在批量访问时是灾难。
于拦截中见时机,于缓存中见复用;以预加载为尺,解 N+1 之牛,于数据访问中,求平衡之真。

行动指令

  1. 阅读源码:打开vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php,重点看__get()getRelationshipFromMethod()
  2. 调试 N+1:安装 Laravel Debugbar,故意写出 N+1 代码,观察查询列表。然后加上with(),再次观察。
  3. 查看 Relations:在断点中查看$user->relations数组,理解数据是如何被缓存的。
  4. 思维升级:记住,延迟加载是默认行为,但预加载应该是你的默认选择。除非你确定只访问一次,否则永远使用with()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/8 20:16:02

从执行到战略:采购工程师如何成为供应链核心价值创造者

1. 从“买买买”到战略核心&#xff1a;重新认识采购的价值刚入行那会儿&#xff0c;很多人问我&#xff1a;“采购不就是买东西吗&#xff1f;有什么难的&#xff1f;” 甚至在一些公司内部&#xff0c;采购部门也被简单地视为一个执行部门&#xff0c;负责下单、跟单、付款。…

作者头像 李华
网站建设 2026/6/8 20:16:00

从航空航天到生物医疗:用ABAQUS模拟形状记忆聚合物SMP的‘记忆’魔法

形状记忆聚合物仿真实战&#xff1a;ABAQUS在跨领域创新中的高阶应用形状记忆聚合物&#xff08;SMP&#xff09;正在重塑多个行业的创新边界——从太空中的自展开卫星天线到人体内可降解血管支架&#xff0c;再到能自动调节透气性的智能服装。这种材料最令人着迷的特性在于它能…

作者头像 李华
网站建设 2026/6/8 20:11:14

办公电脑监控软件实测横评与选型避坑指南

一、中小企业普遍存在的办公管理盲区 很多企业管理者都会遇到一种反差感极强的管理现状&#xff1a;工作日办公室内全员在岗&#xff0c;员工键盘敲击不停、看似专注办公&#xff0c;整体工作氛围十分饱满。但月末复盘业绩时&#xff0c;核心指标却持续低迷、毫无起色。 这其实…

作者头像 李华
网站建设 2026/6/8 20:09:14

Pearcleaner:免费开源macOS终极清理工具,彻底告别应用残留

Pearcleaner&#xff1a;免费开源macOS终极清理工具&#xff0c;彻底告别应用残留 【免费下载链接】Pearcleaner A free, source-available and fair-code licensed mac app cleaner 项目地址: https://gitcode.com/gh_mirrors/pe/Pearcleaner 关键词&#xff1a;macOS清…

作者头像 李华