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

发布时间:2026/6/8 20:16:27

laravel的延迟加载的源码解读的庖丁解牛 在 Laravel 中“延迟加载”通常指两个层面的概念但源码机制截然不同Eloquent 关联关系的延迟加载最常用也是性能陷阱所在访问$user-posts时才去查数据库。服务容器的延迟加载服务只有在第一次被make()时才实例化。鉴于前文已深入探讨过容器这里我们聚焦于Eloquent 关联关系的延迟加载这是 Laravel ORM 中最具魔力也最危险的特性。它的本质是**延迟加载是一种“用时间换空间/便利性”的策略。它通过魔术方法 (__get)拦截属性访问在运行时动态发起数据库查询将关联数据填充到模型中。核心矛盾用户希望像访问普通属性一样访问关联数据$user-posts但关联数据不在内存中而在数据库里。解决方案当代码尝试读取不存在的属性时Laravel 检查这是否是一个定义的关联关系。如果是它立即执行 SQL 查询获取结果缓存到模型内部然后返回。核心逻辑别把延迟加载当成“智能预知”。它是“被动触发”。你不去摸它它就不动你一摸它就跑去数据库搬砖。如果在循环里摸它就跑断腿N1。如果把延迟加载比作点菜预加载 (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. 判断逻辑步骤检查属性array_key_exists($key, $this-attributes)如果是直接返回值。检查关联method_exists($this, $key)或者更准确地说检查是否有名为$key的方法且该方法返回Relation对象。触发加载如果确认为关联调用$this-getRelationshipFromMethod($key)。 核心洞察__get是延迟加载的开关。它将“属性访问”语义转换为“方法调用”语义。二、关联解析流程从方法到查询1. 获取关联对象getRelationshipFromMethod()代码位置Model::getRelationshipFromMethod($method)动作调用$this-$method()。注意这里是调用方法而不是访问属性。例如$this-posts()返回一个HasMany关系对象。关键点此时还没有执行 SQL。HasMany对象只是持有外键信息和查询构建器。2. 执行查询getResults()代码位置Illuminate\Database\Eloquent\Relations\Relation::getResults()动作HasMany继承自Relation。调用$this-query-get()。这里触发了Query Builder的执行生成 SQL 并查询数据库。返回Collection结果。3. 存入模型setRelation()代码位置Model::setRelation($relation, $value)动作将查询结果存入模型的$relations数组$this-relations[$method] $value。价值下次再访问$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四、N1 问题的根源源码视角的悲剧为什么延迟加载会导致 N1场景$usersUser::all();// 1 queryforeach($usersas$user){echo$user-posts-count();// N queries}源码分析User::all()返回 100 个User模型实例。此时它们的$relations数组是空的。进入循环。第一次迭代访问$user-posts。触发__get。检查$relations[posts]-空。执行getRelationshipFromMethod-查库(Query #2)。缓存结果。第二次迭代访问另一个$user对象的-posts。触发__get。检查该对象的$relations[posts]-空(因为每个模型实例是独立的)。执行getRelationshipFromMethod-查库(Query #3)。…重复 100 次。 核心洞察N1 的本质是对象隔离性。每个模型实例不知道其他实例的需求因此各自为战独立发起查询。五、对比预加载 (Eager Loading) 如何打破魔咒1. 入口with()代码User::with(posts)-get()机制先查询所有 Users。收集所有 User 的 ID。执行一次查询SELECT * FROM posts WHERE user_id IN (1, 2, ... 100)。关键步骤MatchThroughRelations。遍历查询结果。根据user_id找到对应的User模型实例。调用$user-setRelation(posts, $matchedPosts)。此时所有 User 模型的$relations[posts]都已填满。2. 访问时循环中访问$user-posts。__get检查$relations-有数据。直接返回零查询。 总结原子化“Laravel 延迟加载”全景图维度关键点本质基于__get拦截的按需数据库查询机制核心触发访问未加载的关联属性 -__get-getRelationshipFromMethod缓存机制结果存入$model-relations数组避免重复查询性能陷阱N1 问题循环中访问不同实例的关联导致多次查询解决方案预加载 (with())提前批量查询并填充$relations源码核心类Model,HasMany(etc.),Relation,BuilderPHP 隐喻Ordering Food on Demand (Lazy) vs. Buffet Set (Eager)公式Load (Intercept × Query) ^ Cache终极心法延迟加载的本质是“懒惰的智慧”。它不预先做任何事直到被需要。这种懒惰在单次访问时是高效的但在批量访问时是灾难。于拦截中见时机于缓存中见复用以预加载为尺解 N1 之牛于数据访问中求平衡之真。行动指令阅读源码打开vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php重点看__get()和getRelationshipFromMethod()。调试 N1安装 Laravel Debugbar故意写出 N1 代码观察查询列表。然后加上with()再次观察。查看 Relations在断点中查看$user-relations数组理解数据是如何被缓存的。思维升级记住延迟加载是默认行为但预加载应该是你的默认选择。除非你确定只访问一次否则永远使用with()。

相关新闻